// Copyright Epic Games, Inc. All Rights Reserved.

#include "projectstore.h"

#include <zencore/assertfmt.h>
#include <zencore/compactbinarybuilder.h>
#include <zencore/compactbinarypackage.h>
#include <zencore/compactbinaryutil.h>
#include <zencore/compactbinaryvalidation.h>
#include <zencore/filesystem.h>
#include <zencore/fmtutils.h>
#include <zencore/jobqueue.h>
#include <zencore/logging.h>
#include <zencore/scopeguard.h>
#include <zencore/stream.h>
#include <zencore/timer.h>
#include <zencore/trace.h>
#include <zenstore/caslog.h>
#include <zenstore/cidstore.h>
#include <zenstore/scrubcontext.h>
#include <zenutil/cache/rpcrecording.h>
#include <zenutil/packageformat.h>
#include <zenutil/workerpools.h>

#include "fileremoteprojectstore.h"
#include "jupiterremoteprojectstore.h"
#include "remoteprojectstore.h"
#include "zenremoteprojectstore.h"

ZEN_THIRD_PARTY_INCLUDES_START
#include <cpr/cpr.h>
#include <tsl/robin_set.h>
#include <xxh3.h>
ZEN_THIRD_PARTY_INCLUDES_END

#if ZEN_WITH_TESTS
#	include <zencore/testing.h>
#	include <zencore/testutils.h>
#endif	// ZEN_WITH_TESTS

namespace zen {

namespace {
	bool PrepareDirectoryDelete(const std::filesystem::path& Dir, std::filesystem::path& OutDeleteDir)
	{
		int DropIndex = 0;
		do
		{
			if (!std::filesystem::exists(Dir))
			{
				return true;
			}

			std::string			  DroppedName		= fmt::format("[dropped]{}({})", Dir.filename().string(), DropIndex);
			std::filesystem::path DroppedBucketPath = Dir.parent_path() / DroppedName;
			if (std::filesystem::exists(DroppedBucketPath))
			{
				DropIndex++;
				continue;
			}

			std::error_code Ec;
			std::filesystem::rename(Dir, DroppedBucketPath, Ec);
			if (!Ec)
			{
				OutDeleteDir = DroppedBucketPath;
				return true;
			}
			if (Ec && !std::filesystem::exists(DroppedBucketPath))
			{
				// We can't move our folder, probably because it is busy, bail..
				return false;
			}
			Sleep(100);
		} while (true);
	}

	struct CreateRemoteStoreResult
	{
		std::shared_ptr<RemoteProjectStore> Store;
		std::string							Description;
	};
	CreateRemoteStoreResult CreateRemoteStore(CbObjectView				   Params,
											  AuthMgr&					   AuthManager,
											  size_t					   MaxBlockSize,
											  size_t					   MaxChunkEmbedSize,
											  const std::filesystem::path& TempFilePath)
	{
		using namespace std::literals;

		std::shared_ptr<RemoteProjectStore> RemoteStore;

		if (CbObjectView File = Params["file"sv].AsObjectView(); File)
		{
			std::filesystem::path FolderPath(File["path"sv].AsString());
			if (FolderPath.empty())
			{
				return {nullptr, "Missing file path"};
			}
			std::string_view Name(File["name"sv].AsString());
			if (Name.empty())
			{
				return {nullptr, "Missing file name"};
			}
			std::string_view OptionalBaseName(File["basename"sv].AsString());
			bool			 ForceDisableBlocks	   = File["disableblocks"sv].AsBool(false);
			bool			 ForceEnableTempBlocks = File["enabletempblocks"sv].AsBool(false);

			FileRemoteStoreOptions Options = {RemoteStoreOptions{.MaxBlockSize = MaxBlockSize, .MaxChunkEmbedSize = MaxChunkEmbedSize},
											  FolderPath,
											  std::string(Name),
											  std::string(OptionalBaseName),
											  ForceDisableBlocks,
											  ForceEnableTempBlocks};
			RemoteStore					   = CreateFileRemoteStore(Options);
		}

		if (CbObjectView Cloud = Params["cloud"sv].AsObjectView(); Cloud)
		{
			std::string_view CloudServiceUrl = Cloud["url"sv].AsString();
			if (CloudServiceUrl.empty())
			{
				return {nullptr, "Missing service url"};
			}

			std::string		 Url	   = cpr::util::urlDecode(std::string(CloudServiceUrl));
			std::string_view Namespace = Cloud["namespace"sv].AsString();
			if (Namespace.empty())
			{
				return {nullptr, "Missing namespace"};
			}
			std::string_view Bucket = Cloud["bucket"sv].AsString();
			if (Bucket.empty())
			{
				return {nullptr, "Missing bucket"};
			}
			std::string_view OpenIdProvider = Cloud["openid-provider"sv].AsString();
			std::string		 AccessToken	= std::string(Cloud["access-token"sv].AsString());
			if (AccessToken.empty())
			{
				std::string_view AccessTokenEnvVariable = Cloud["access-token-env"].AsString();
				if (!AccessTokenEnvVariable.empty())
				{
					AccessToken = GetEnvVariable(AccessTokenEnvVariable);
				}
			}
			std::string_view KeyParam = Cloud["key"sv].AsString();
			if (KeyParam.empty())
			{
				return {nullptr, "Missing key"};
			}
			if (KeyParam.length() != IoHash::StringLength)
			{
				return {nullptr, "Invalid key"};
			}
			IoHash Key = IoHash::FromHexString(KeyParam);
			if (Key == IoHash::Zero)
			{
				return {nullptr, "Invalid key string"};
			}
			IoHash			 BaseKey	  = IoHash::Zero;
			std::string_view BaseKeyParam = Cloud["basekey"sv].AsString();
			if (!BaseKeyParam.empty())
			{
				if (BaseKeyParam.length() != IoHash::StringLength)
				{
					return {nullptr, "Invalid base key"};
				}
				BaseKey = IoHash::FromHexString(BaseKeyParam);
				if (BaseKey == IoHash::Zero)
				{
					return {nullptr, "Invalid base key string"};
				}
			}

			bool ForceDisableBlocks		= Cloud["disableblocks"sv].AsBool(false);
			bool ForceDisableTempBlocks = Cloud["disabletempblocks"sv].AsBool(false);
			bool AssumeHttp2			= Cloud["assumehttp2"sv].AsBool(false);

			JupiterRemoteStoreOptions Options = {RemoteStoreOptions{.MaxBlockSize = MaxBlockSize, .MaxChunkEmbedSize = MaxChunkEmbedSize},
												 Url,
												 std::string(Namespace),
												 std::string(Bucket),
												 Key,
												 BaseKey,
												 std::string(OpenIdProvider),
												 AccessToken,
												 AuthManager,
												 ForceDisableBlocks,
												 ForceDisableTempBlocks,
												 AssumeHttp2};
			RemoteStore						  = CreateJupiterRemoteStore(Options, TempFilePath);
		}

		if (CbObjectView Zen = Params["zen"sv].AsObjectView(); Zen)
		{
			std::string_view Url	 = Zen["url"sv].AsString();
			std::string_view Project = Zen["project"sv].AsString();
			if (Project.empty())
			{
				return {nullptr, "Missing project"};
			}
			std::string_view Oplog = Zen["oplog"sv].AsString();
			if (Oplog.empty())
			{
				return {nullptr, "Missing oplog"};
			}
			ZenRemoteStoreOptions Options = {RemoteStoreOptions{.MaxBlockSize = MaxBlockSize, .MaxChunkEmbedSize = MaxChunkEmbedSize},
											 std::string(Url),
											 std::string(Project),
											 std::string(Oplog)};
			RemoteStore					  = CreateZenRemoteStore(Options, TempFilePath);
		}

		if (!RemoteStore)
		{
			return {nullptr, "Unknown remote store type"};
		}

		return {std::move(RemoteStore), ""};
	}

	std::pair<HttpResponseCode, std::string> ConvertResult(const RemoteProjectStore::Result& Result)
	{
		if (Result.ErrorCode == 0)
		{
			return {HttpResponseCode::OK, Result.Text};
		}
		return {static_cast<HttpResponseCode>(Result.ErrorCode),
				Result.Reason.empty() ? Result.Text
				: Result.Text.empty() ? Result.Reason
									  : fmt::format("{}: {}", Result.Reason, Result.Text)};
	}

}  // namespace

//////////////////////////////////////////////////////////////////////////

struct ProjectStore::OplogStorage : public RefCounted
{
	OplogStorage(ProjectStore::Oplog* OwnerOplog, std::filesystem::path BasePath) : m_OwnerOplog(OwnerOplog), m_OplogStoragePath(BasePath)
	{
	}

	~OplogStorage()
	{
		ZEN_INFO("closing oplog storage at {}", m_OplogStoragePath);
		Flush();
	}

	[[nodiscard]] bool		  Exists() const { return Exists(m_OplogStoragePath); }
	[[nodiscard]] static bool Exists(const std::filesystem::path& BasePath)
	{
		return std::filesystem::exists(BasePath / "ops.zlog") && std::filesystem::exists(BasePath / "ops.zops");
	}

	static bool Delete(const std::filesystem::path& BasePath) { return DeleteDirectories(BasePath); }

	uint64_t		OpBlobsSize() const { return OpBlobsSize(m_OplogStoragePath); }
	static uint64_t OpBlobsSize(const std::filesystem::path& BasePath)
	{
		using namespace std::literals;
		if (Exists(BasePath))
		{
			return std::filesystem::file_size(BasePath / "ops.zlog"sv) + std::filesystem::file_size(BasePath / "ops.zops"sv);
		}
		return 0;
	}

	void Open(bool IsCreate)
	{
		ZEN_TRACE_CPU("Store::OplogStorage::Open");

		using namespace std::literals;

		ZEN_INFO("initializing oplog storage at '{}'", m_OplogStoragePath);

		if (IsCreate)
		{
			DeleteDirectories(m_OplogStoragePath);
			CreateDirectories(m_OplogStoragePath);
		}

		m_Oplog.Open(m_OplogStoragePath / "ops.zlog"sv, IsCreate ? CasLogFile::Mode::kTruncate : CasLogFile::Mode::kWrite);
		m_Oplog.Initialize();

		m_OpBlobs.Open(m_OplogStoragePath / "ops.zops"sv, IsCreate ? BasicFile::Mode::kTruncate : BasicFile::Mode::kWrite);

		ZEN_ASSERT(IsPow2(m_OpsAlign));
		ZEN_ASSERT(!(m_NextOpsOffset & (m_OpsAlign - 1)));
	}

	void ReplayLog(std::function<void(CbObjectView, const OplogEntry&)>&& Handler)
	{
		ZEN_TRACE_CPU("Store::OplogStorage::ReplayLog");

		// This could use memory mapping or do something clever but for now it just reads the file sequentially

		ZEN_INFO("replaying log for '{}'", m_OplogStoragePath);

		uint64_t OpsBlockSize = m_OpBlobs.FileSize();

		Stopwatch Timer;

		uint64_t InvalidEntries = 0;

		std::vector<OplogEntry> OpLogEntries;
		std::vector<size_t>		OplogOrder;
		{
			tsl::robin_map<Oid, size_t, Oid::Hasher> LatestKeys;
			const uint64_t							 SkipEntryCount = 0;

			m_Oplog.Replay(
				[&](const OplogEntry& LogEntry) {
					if (LogEntry.IsTombstone())
					{
						if (auto It = LatestKeys.find(LogEntry.OpKeyHash); It == LatestKeys.end())
						{
							ZEN_SCOPED_WARN("found tombstone referencing unknown key {}", LogEntry.OpKeyHash);
							++InvalidEntries;
							return;
						}
					}
					else if (LogEntry.OpCoreSize == 0)
					{
						ZEN_SCOPED_WARN("skipping zero size op {}", LogEntry.OpKeyHash);
						++InvalidEntries;
						return;
					}
					else if (LogEntry.OpLsn == 0)
					{
						ZEN_SCOPED_WARN("skipping zero lsn op {}", LogEntry.OpKeyHash);
						++InvalidEntries;
						return;
					}

					const uint64_t OpFileOffset = LogEntry.OpCoreOffset * m_OpsAlign;
					if ((OpFileOffset + LogEntry.OpCoreSize) > OpsBlockSize)
					{
						ZEN_SCOPED_WARN("skipping out of bounds op {}", LogEntry.OpKeyHash);
						++InvalidEntries;
						return;
					}

					if (auto It = LatestKeys.find(LogEntry.OpKeyHash); It != LatestKeys.end())
					{
						OplogEntry& Entry = OpLogEntries[It->second];

						if (LogEntry.IsTombstone() && Entry.IsTombstone())
						{
							ZEN_SCOPED_WARN("found double tombstone - {}", LogEntry.OpKeyHash);
						}

						Entry = LogEntry;
					}
					else
					{
						const size_t OpIndex		   = OpLogEntries.size();
						LatestKeys[LogEntry.OpKeyHash] = OpIndex;
						OplogOrder.push_back(OpIndex);
						OpLogEntries.push_back(LogEntry);
					}
				},
				SkipEntryCount);
		}

		std::sort(OplogOrder.begin(), OplogOrder.end(), [&](size_t Lhs, size_t Rhs) {
			const OplogEntry& LhsEntry = OpLogEntries[Lhs];
			const OplogEntry& RhsEntry = OpLogEntries[Rhs];
			return LhsEntry.OpCoreOffset < RhsEntry.OpCoreOffset;
		});

		uint64_t TombstoneEntries = 0;

		BasicFileBuffer OpBlobsBuffer(m_OpBlobs, 65536);

		uint32_t MaxOpLsn		  = m_MaxLsn.load(std::memory_order_relaxed);
		uint64_t NextOpFileOffset = m_NextOpsOffset.load(std::memory_order_relaxed);

		for (size_t OplogOrderIndex : OplogOrder)
		{
			const OplogEntry& LogEntry = OpLogEntries[OplogOrderIndex];

			if (LogEntry.IsTombstone())
			{
				TombstoneEntries++;
			}
			else
			{
				// Verify checksum, ignore op data if incorrect

				auto VerifyAndHandleOp = [&](MemoryView OpBufferView) {
					const uint32_t ExpectedOpCoreHash = LogEntry.OpCoreHash;
					const uint32_t OpCoreHash		  = uint32_t(XXH3_64bits(OpBufferView.GetData(), LogEntry.OpCoreSize) & 0xffffFFFF);

					if (OpCoreHash != ExpectedOpCoreHash)
					{
						ZEN_WARN("skipping bad checksum op - {}. Expected: {}, found: {}",
								 LogEntry.OpKeyHash,
								 ExpectedOpCoreHash,
								 OpCoreHash);
					}
					else if (CbValidateError Err = ValidateCompactBinary(OpBufferView, CbValidateMode::Default);
							 Err != CbValidateError::None)
					{
						ZEN_WARN("skipping invalid format op - {}. Error: '{}'", LogEntry.OpKeyHash, ToString(Err));
					}
					else
					{
						Handler(CbObjectView(OpBufferView.GetData()), LogEntry);
						MaxOpLsn = Max(MaxOpLsn, LogEntry.OpLsn);
						const uint64_t EntryNextOpFileOffset =
							RoundUp((LogEntry.OpCoreOffset * m_OpsAlign) + LogEntry.OpCoreSize, m_OpsAlign);
						NextOpFileOffset = Max(NextOpFileOffset, EntryNextOpFileOffset);
					}
				};

				const uint64_t	 OpFileOffset = LogEntry.OpCoreOffset * m_OpsAlign;
				const MemoryView OpBufferView = OpBlobsBuffer.MakeView(LogEntry.OpCoreSize, OpFileOffset);
				if (OpBufferView.GetSize() == LogEntry.OpCoreSize)
				{
					VerifyAndHandleOp(OpBufferView);
				}
				else
				{
					IoBuffer OpBuffer(LogEntry.OpCoreSize);
					OpBlobsBuffer.Read((void*)OpBuffer.Data(), LogEntry.OpCoreSize, OpFileOffset);

					VerifyAndHandleOp(OpBuffer);
				}
			}
		}

		m_NextOpsOffset = NextOpFileOffset;
		m_MaxLsn		= MaxOpLsn;

		ZEN_INFO("oplog replay completed in {} - Max LSN# {}, Next offset: {}, {} tombstones, {} invalid entries",
				 NiceTimeSpanMs(Timer.GetElapsedTimeMs()),
				 m_MaxLsn.load(),
				 m_NextOpsOffset.load(),
				 TombstoneEntries,
				 InvalidEntries);
	}

	void ReplayLogEntries(const std::span<OplogEntryAddress> Entries, std::function<void(CbObjectView)>&& Handler)
	{
		ZEN_TRACE_CPU("Store::OplogStorage::ReplayLogEntries");

		BasicFileBuffer OpBlobsBuffer(m_OpBlobs, 65536);

		for (const OplogEntryAddress& Entry : Entries)
		{
			const uint64_t OpFileOffset = Entry.Offset * m_OpsAlign;
			MemoryView	   OpBufferView = OpBlobsBuffer.MakeView(Entry.Size, OpFileOffset);
			if (OpBufferView.GetSize() == Entry.Size)
			{
				Handler(CbObjectView(OpBufferView.GetData()));
				continue;
			}
			IoBuffer OpBuffer(Entry.Size);
			OpBlobsBuffer.Read((void*)OpBuffer.Data(), Entry.Size, OpFileOffset);
			Handler(CbObjectView(OpBuffer.Data()));
		}
	}

	CbObject GetOp(const OplogEntryAddress& Entry)
	{
		ZEN_TRACE_CPU("Store::OplogStorage::GetOp");

		IoBuffer OpBuffer(Entry.Size);

		const uint64_t OpFileOffset = Entry.Offset * m_OpsAlign;
		m_OpBlobs.Read((void*)OpBuffer.Data(), Entry.Size, OpFileOffset);

		return CbObject(SharedBuffer(std::move(OpBuffer)));
	}

	struct AppendOpData
	{
		MemoryView Buffer;
		uint32_t   OpCoreHash;
		Oid		   KeyHash;
	};

	static OplogStorage::AppendOpData GetAppendOpData(const CbObjectView& Core)
	{
		using namespace std::literals;

		AppendOpData OpData;

		OpData.Buffer			 = Core.GetView();
		const uint64_t WriteSize = OpData.Buffer.GetSize();
		OpData.OpCoreHash		 = uint32_t(XXH3_64bits(OpData.Buffer.GetData(), WriteSize) & 0xffffFFFF);

		ZEN_ASSERT(WriteSize != 0);

		XXH3_128Stream KeyHasher;
		Core["key"sv].WriteToStream([&](const void* Data, size_t Size) { KeyHasher.Append(Data, Size); });
		XXH3_128 KeyHash128 = KeyHasher.GetHash();
		memcpy(&OpData.KeyHash, KeyHash128.Hash, sizeof OpData.KeyHash);

		return OpData;
	}

	OplogEntry AppendOp(const AppendOpData& OpData)
	{
		ZEN_TRACE_CPU("Store::OplogStorage::AppendOp");

		using namespace std::literals;

		uint64_t WriteSize = OpData.Buffer.GetSize();

		RwLock::ExclusiveLockScope Lock(m_RwLock);
		const uint64_t			   WriteOffset = m_NextOpsOffset;
		const uint32_t			   OpLsn	   = ++m_MaxLsn;
		m_NextOpsOffset						   = RoundUp(WriteOffset + WriteSize, m_OpsAlign);
		Lock.ReleaseNow();

		ZEN_ASSERT(IsMultipleOf(WriteOffset, m_OpsAlign));

		OplogEntry Entry = {.OpLsn		  = OpLsn,
							.OpCoreOffset = gsl::narrow_cast<uint32_t>(WriteOffset / m_OpsAlign),
							.OpCoreSize	  = uint32_t(WriteSize),
							.OpCoreHash	  = OpData.OpCoreHash,
							.OpKeyHash	  = OpData.KeyHash};

		m_Oplog.Append(Entry);
		m_OpBlobs.Write(OpData.Buffer.GetData(), WriteSize, WriteOffset);

		return Entry;
	}

	std::vector<OplogEntry> AppendOps(std::span<const AppendOpData> Ops)
	{
		ZEN_TRACE_CPU("Store::OplogStorage::AppendOps");

		using namespace std::literals;

		size_t									   OpCount = Ops.size();
		std::vector<std::pair<uint64_t, uint64_t>> OffsetAndSizes;
		std::vector<uint32_t>					   OpLsns;
		OffsetAndSizes.resize(OpCount);
		OpLsns.resize(OpCount);

		for (size_t OpIndex = 0; OpIndex < OpCount; OpIndex++)
		{
			OffsetAndSizes[OpIndex].second = Ops[OpIndex].Buffer.GetSize();
		}

		uint64_t WriteStart	 = 0;
		uint64_t WriteLength = 0;
		{
			RwLock::ExclusiveLockScope Lock(m_RwLock);
			WriteStart = m_NextOpsOffset;
			ZEN_ASSERT(IsMultipleOf(WriteStart, m_OpsAlign));
			uint64_t WriteOffset = WriteStart;
			for (size_t OpIndex = 0; OpIndex < OpCount; OpIndex++)
			{
				OffsetAndSizes[OpIndex].first = WriteOffset - WriteStart;
				OpLsns[OpIndex]				  = ++m_MaxLsn;
				WriteOffset					  = RoundUp(WriteOffset + OffsetAndSizes[OpIndex].second, m_OpsAlign);
			}
			WriteLength		= WriteOffset - WriteStart;
			m_NextOpsOffset = RoundUp(WriteOffset, m_OpsAlign);
		}

		IoBuffer WriteBuffer(WriteLength);

		std::vector<OplogEntry> Entries;
		Entries.resize(OpCount);
		for (size_t OpIndex = 0; OpIndex < OpCount; OpIndex++)
		{
			MutableMemoryView WriteBufferView = WriteBuffer.GetMutableView().RightChop(OffsetAndSizes[OpIndex].first);
			WriteBufferView.CopyFrom(Ops[OpIndex].Buffer);
			Entries[OpIndex] = {.OpLsn		  = OpLsns[OpIndex],
								.OpCoreOffset = gsl::narrow_cast<uint32_t>((WriteStart + OffsetAndSizes[OpIndex].first) / m_OpsAlign),
								.OpCoreSize	  = uint32_t(OffsetAndSizes[OpIndex].second),
								.OpCoreHash	  = Ops[OpIndex].OpCoreHash,
								.OpKeyHash	  = Ops[OpIndex].KeyHash};
		}

		m_OpBlobs.Write(WriteBuffer.GetData(), WriteBuffer.GetSize(), WriteStart);
		m_Oplog.Append(Entries);

		return Entries;
	}

	void AppendTombstone(Oid KeyHash)
	{
		OplogEntry Entry = {.OpKeyHash = KeyHash};
		Entry.MakeTombstone();

		m_Oplog.Append(Entry);
	}

	void Flush()
	{
		m_Oplog.Flush();
		m_OpBlobs.Flush();
	}

	uint32_t GetMaxLsn() const { return m_MaxLsn.load(); }

	LoggerRef Log() { return m_OwnerOplog->Log(); }

private:
	ProjectStore::Oplog*	m_OwnerOplog;
	std::filesystem::path	m_OplogStoragePath;
	mutable RwLock			m_RwLock;
	TCasLogFile<OplogEntry> m_Oplog;
	BasicFile				m_OpBlobs;
	std::atomic<uint64_t>	m_NextOpsOffset{0};
	uint64_t				m_OpsAlign = 32;
	std::atomic<uint32_t>	m_MaxLsn{0};
};

//////////////////////////////////////////////////////////////////////////

ProjectStore::Oplog::Oplog(std::string_view				Id,
						   Project*						Project,
						   CidStore&					Store,
						   std::filesystem::path		BasePath,
						   const std::filesystem::path& MarkerPath)
: m_OuterProject(Project)
, m_CidStore(Store)
, m_BasePath(BasePath)
, m_MarkerPath(MarkerPath)
, m_OplogId(Id)
{
	using namespace std::literals;

	m_Storage			   = new OplogStorage(this, m_BasePath);
	const bool StoreExists = m_Storage->Exists();
	m_Storage->Open(/* IsCreate */ !StoreExists);

	m_TempPath = m_BasePath / "temp"sv;

	CleanDirectory(m_TempPath);
}

ProjectStore::Oplog::~Oplog()
{
	if (m_Storage)
	{
		Flush();
	}
}

void
ProjectStore::Oplog::Flush()
{
	ZEN_ASSERT(m_Storage);
	m_Storage->Flush();
}

void
ProjectStore::Oplog::ScrubStorage(ScrubContext& Ctx)
{
	std::vector<Oid> BadEntryKeys;

	using namespace std::literals;

	IterateOplogWithKey([&](int Lsn, const Oid& Key, CbObjectView Op) {
		ZEN_UNUSED(Lsn);

		std::vector<IoHash> Cids;
		Op.IterateAttachments([&](CbFieldView Visitor) { Cids.emplace_back(Visitor.AsAttachment()); });

		{
			XXH3_128Stream KeyHasher;
			Op["key"sv].WriteToStream([&](const void* Data, size_t Size) { KeyHasher.Append(Data, Size); });
			XXH3_128 KeyHash128 = KeyHasher.GetHash();
			Oid		 KeyHash;
			memcpy(&KeyHash, KeyHash128.Hash, sizeof KeyHash);

			ZEN_ASSERT_FORMAT(KeyHash == Key, "oplog data does not match information from index (op:{} != index:{})", KeyHash, Key);
		}

		for (const IoHash& Cid : Cids)
		{
			if (!m_CidStore.ContainsChunk(Cid))
			{
				// oplog entry references a CAS chunk which is not
				// present
				BadEntryKeys.push_back(Key);
				return;
			}
			if (Ctx.IsBadCid(Cid))
			{
				// oplog entry references a CAS chunk which has been
				// flagged as bad
				BadEntryKeys.push_back(Key);
				return;
			}
		}
	});

	if (!BadEntryKeys.empty())
	{
		if (Ctx.RunRecovery())
		{
			ZEN_WARN("scrubbing found {} bad ops in oplog @ '{}', these will be removed from the index", BadEntryKeys.size(), m_BasePath);

			// Actually perform some clean-up
			RwLock::ExclusiveLockScope _(m_OplogLock);

			for (const auto& Key : BadEntryKeys)
			{
				m_LatestOpMap.erase(Key);
				m_Storage->AppendTombstone(Key);
			}
		}
		else
		{
			ZEN_WARN("scrubbing found {} bad ops in oplog @ '{}' but no cleanup will be performed", BadEntryKeys.size(), m_BasePath);
		}
	}
}

void
ProjectStore::Oplog::GatherReferences(GcContext& GcCtx)
{
	ZEN_TRACE_CPU("Store::Oplog::GatherReferences");
	if (GcCtx.SkipCid())
	{
		return;
	}

	std::vector<IoHash> Cids;
	Cids.reserve(1024);
	IterateOplog([&](CbObjectView Op) {
		Op.IterateAttachments([&](CbFieldView Visitor) { Cids.emplace_back(Visitor.AsAttachment()); });
		if (Cids.size() >= 1024)
		{
			GcCtx.AddRetainedCids(Cids);
			Cids.clear();
		}
	});
	GcCtx.AddRetainedCids(Cids);
}

uint64_t
ProjectStore::Oplog::TotalSize(const std::filesystem::path& BasePath)
{
	using namespace std::literals;

	uint64_t			  Size			= OplogStorage::OpBlobsSize(BasePath);
	std::filesystem::path StateFilePath = BasePath / "oplog.zcb"sv;
	if (std::filesystem::exists(StateFilePath))
	{
		Size += std::filesystem::file_size(StateFilePath);
	}

	return Size;
}

uint64_t
ProjectStore::Oplog::TotalSize() const
{
	return TotalSize(m_BasePath);
}

std::filesystem::path
ProjectStore::Oplog::PrepareForDelete(bool MoveFolder)
{
	RwLock::ExclusiveLockScope _(m_OplogLock);
	m_ChunkMap.clear();
	m_MetaMap.clear();
	m_FileMap.clear();
	m_OpAddressMap.clear();
	m_LatestOpMap.clear();
	m_Storage = {};
	if (!MoveFolder)
	{
		return {};
	}
	std::filesystem::path MovedDir;
	if (PrepareDirectoryDelete(m_BasePath, MovedDir))
	{
		return MovedDir;
	}
	return {};
}

bool
ProjectStore::Oplog::ExistsAt(const std::filesystem::path& BasePath)
{
	using namespace std::literals;

	std::filesystem::path StateFilePath = BasePath / "oplog.zcb"sv;
	return std::filesystem::is_regular_file(StateFilePath);
}

void
ProjectStore::Oplog::Read()
{
	using namespace std::literals;

	std::filesystem::path StateFilePath = m_BasePath / "oplog.zcb"sv;
	if (std::filesystem::is_regular_file(StateFilePath))
	{
		ZEN_INFO("reading config for oplog '{}' in project '{}' from {}", m_OplogId, m_OuterProject->Identifier, StateFilePath);

		BasicFile Blob;
		Blob.Open(StateFilePath, BasicFile::Mode::kRead);

		IoBuffer		Obj				= Blob.ReadAll();
		CbValidateError ValidationError = ValidateCompactBinary(MemoryView(Obj.Data(), Obj.Size()), CbValidateMode::All);

		if (ValidationError != CbValidateError::None)
		{
			ZEN_ERROR("validation error {} hit for '{}'", int(ValidationError), StateFilePath);
			return;
		}

		CbObject Cfg = LoadCompactBinaryObject(Obj);

		m_MarkerPath = Cfg["gcpath"sv].AsU8String();
	}
	else
	{
		ZEN_INFO("config for oplog '{}' in project '{}' not found at {}. Assuming legacy store",
				 m_OplogId,
				 m_OuterProject->Identifier,
				 StateFilePath);
	}
	ReplayLog();
}

void
ProjectStore::Oplog::Write()
{
	using namespace std::literals;

	BinaryWriter Mem;

	CbObjectWriter Cfg;

	Cfg << "gcpath"sv << PathToUtf8(m_MarkerPath);

	Cfg.Save(Mem);

	std::filesystem::path StateFilePath = m_BasePath / "oplog.zcb"sv;

	ZEN_INFO("persisting config for oplog '{}' in project '{}' to {}", m_OplogId, m_OuterProject->Identifier, StateFilePath);

	TemporaryFile::SafeWriteFile(StateFilePath, Mem.GetView());
}

void
ProjectStore::Oplog::Update(const std::filesystem::path& MarkerPath)
{
	if (m_MarkerPath == MarkerPath)
	{
		return;
	}
	Write();
}

bool
ProjectStore::Oplog::Reset()
{
	std::filesystem::path MovedDir;

	{
		RwLock::ExclusiveLockScope OplogLock(m_OplogLock);
		m_Storage = {};
		if (!PrepareDirectoryDelete(m_BasePath, MovedDir))
		{
			m_Storage			   = new OplogStorage(this, m_BasePath);
			const bool StoreExists = m_Storage->Exists();
			m_Storage->Open(/* IsCreate */ !StoreExists);
			return false;
		}
		m_ChunkMap.clear();
		m_MetaMap.clear();
		m_FileMap.clear();
		m_OpAddressMap.clear();
		m_LatestOpMap.clear();
		m_Storage = new OplogStorage(this, m_BasePath);
		m_Storage->Open(true);
		CleanDirectory(m_TempPath);
		Write();
	}
	// Erase content on disk
	if (!MovedDir.empty())
	{
		OplogStorage::Delete(MovedDir);
	}
	return true;
}

void
ProjectStore::Oplog::ReplayLog()
{
	ZEN_LOG_SCOPE("ReplayLog '{}'", m_OplogId);

	RwLock::ExclusiveLockScope OplogLock(m_OplogLock);
	if (!m_Storage)
	{
		return;
	}
	m_Storage->ReplayLog([&](CbObjectView Op, const OplogEntry& OpEntry) { RegisterOplogEntry(OplogLock, GetMapping(Op), OpEntry); });
}

IoBuffer
ProjectStore::Oplog::GetChunkByRawHash(const IoHash& RawHash)
{
	return m_CidStore.FindChunkByCid(RawHash);
}

bool
ProjectStore::Oplog::IterateChunks(std::span<IoHash>												 RawHashes,
								   const std::function<bool(size_t Index, const IoBuffer& Payload)>& AsyncCallback,
								   WorkerThreadPool*												 OptionalWorkerPool)
{
	return m_CidStore.IterateChunks(RawHashes, AsyncCallback, OptionalWorkerPool);
}

bool
ProjectStore::Oplog::IterateChunks(std::span<Oid>													 ChunkIds,
								   const std::function<bool(size_t Index, const IoBuffer& Payload)>& AsyncCallback,
								   WorkerThreadPool*												 OptionalWorkerPool)
{
	std::vector<size_t>				   CidChunkIndexes;
	std::vector<IoHash>				   CidChunkHashes;
	std::vector<size_t>				   FileChunkIndexes;
	std::vector<std::filesystem::path> FileChunkPaths;
	{
		RwLock::SharedLockScope OplogLock(m_OplogLock);
		for (size_t ChunkIndex = 0; ChunkIndex < ChunkIds.size(); ChunkIndex++)
		{
			const Oid& ChunkId = ChunkIds[ChunkIndex];
			if (auto ChunkIt = m_ChunkMap.find(ChunkId); ChunkIt != m_ChunkMap.end())
			{
				CidChunkIndexes.push_back(ChunkIndex);
				CidChunkHashes.push_back(ChunkIt->second);
			}
			else if (auto MetaIt = m_MetaMap.find(ChunkId); MetaIt != m_MetaMap.end())
			{
				CidChunkIndexes.push_back(ChunkIndex);
				CidChunkHashes.push_back(ChunkIt->second);
			}
			else if (auto FileIt = m_FileMap.find(ChunkId); FileIt != m_FileMap.end())
			{
				FileChunkIndexes.push_back(ChunkIndex);
				FileChunkPaths.emplace_back(m_OuterProject->RootDir / FileIt->second.ServerPath);
			}
		}
	}
	m_CidStore.IterateChunks(
		CidChunkHashes,
		[&](size_t Index, const IoBuffer& Payload) { return AsyncCallback(CidChunkIndexes[Index], Payload); },
		OptionalWorkerPool);

	if (OptionalWorkerPool)
	{
		std::atomic_bool Result = true;
		Latch			 WorkLatch(1);

		for (size_t ChunkIndex = 0; ChunkIndex < FileChunkIndexes.size(); ChunkIndex++)
		{
			if (Result.load() == false)
			{
				break;
			}
			WorkLatch.AddCount(1);
			OptionalWorkerPool->ScheduleWork([this, &WorkLatch, ChunkIndex, &FileChunkIndexes, &FileChunkPaths, &AsyncCallback, &Result]() {
				auto _ = MakeGuard([&WorkLatch]() { WorkLatch.CountDown(); });
				if (Result.load() == false)
				{
					return;
				}
				size_t						 FileChunkIndex = FileChunkIndexes[ChunkIndex];
				const std::filesystem::path& FilePath		= FileChunkPaths[ChunkIndex];
				try
				{
					IoBuffer Payload = IoBufferBuilder::MakeFromFile(FilePath);
					if (Payload)
					{
						if (!AsyncCallback(FileChunkIndex, Payload))
						{
							Result.store(false);
						}
					}
				}
				catch (const std::exception& Ex)
				{
					ZEN_WARN("Exception caught when iterating file chunk {}, path '{}'. Reason: '{}'", FileChunkIndex, FilePath, Ex.what());
				}
			});
		}

		WorkLatch.CountDown();
		WorkLatch.Wait();

		return Result.load();
	}
	else
	{
		for (size_t ChunkIndex = 0; ChunkIndex < FileChunkIndexes.size(); ChunkIndex++)
		{
			size_t						 FileChunkIndex = FileChunkIndexes[ChunkIndex];
			const std::filesystem::path& FilePath		= FileChunkPaths[ChunkIndex];
			IoBuffer					 Payload		= IoBufferBuilder::MakeFromFile(FilePath);
			if (Payload)
			{
				bool Result = AsyncCallback(FileChunkIndex, Payload);
				if (!Result)
				{
					return false;
				}
			}
		}
	}
	return true;
}

IoBuffer
ProjectStore::Oplog::FindChunk(const Oid& ChunkId)
{
	RwLock::SharedLockScope OplogLock(m_OplogLock);
	if (!m_Storage)
	{
		return IoBuffer{};
	}

	if (auto ChunkIt = m_ChunkMap.find(ChunkId); ChunkIt != m_ChunkMap.end())
	{
		IoHash ChunkHash = ChunkIt->second;
		OplogLock.ReleaseNow();

		return m_CidStore.FindChunkByCid(ChunkHash);
	}

	if (auto FileIt = m_FileMap.find(ChunkId); FileIt != m_FileMap.end())
	{
		std::filesystem::path FilePath = m_OuterProject->RootDir / FileIt->second.ServerPath;

		OplogLock.ReleaseNow();

		return IoBufferBuilder::MakeFromFile(FilePath);
	}

	if (auto MetaIt = m_MetaMap.find(ChunkId); MetaIt != m_MetaMap.end())
	{
		IoHash ChunkHash = MetaIt->second;
		OplogLock.ReleaseNow();

		return m_CidStore.FindChunkByCid(ChunkHash);
	}

	return {};
}

std::vector<ProjectStore::Oplog::ChunkInfo>
ProjectStore::Oplog::GetAllChunksInfo()
{
	// First just capture all the chunk ids

	std::vector<ChunkInfo> InfoArray;

	{
		RwLock::SharedLockScope _(m_OplogLock);

		if (m_Storage)
		{
			const size_t NumEntries = m_FileMap.size() + m_ChunkMap.size();

			InfoArray.reserve(NumEntries);

			for (const auto& Kv : m_FileMap)
			{
				InfoArray.push_back({.ChunkId = Kv.first});
			}

			for (const auto& Kv : m_ChunkMap)
			{
				InfoArray.push_back({.ChunkId = Kv.first});
			}
		}
	}

	for (ChunkInfo& Info : InfoArray)
	{
		if (IoBuffer Chunk = FindChunk(Info.ChunkId))
		{
			Info.ChunkSize = Chunk.GetSize();
		}
	}

	return InfoArray;
}

void
ProjectStore::Oplog::IterateChunkMap(std::function<void(const Oid&, const IoHash&)>&& Fn)
{
	RwLock::SharedLockScope _(m_OplogLock);
	if (!m_Storage)
	{
		return;
	}

	for (const auto& Kv : m_ChunkMap)
	{
		Fn(Kv.first, Kv.second);
	}
}

void
ProjectStore::Oplog::IterateFileMap(
	std::function<void(const Oid&, const std::string_view& ServerPath, const std::string_view& ClientPath)>&& Fn)
{
	RwLock::SharedLockScope _(m_OplogLock);
	if (!m_Storage)
	{
		return;
	}

	for (const auto& Kv : m_FileMap)
	{
		Fn(Kv.first, Kv.second.ServerPath, Kv.second.ClientPath);
	}
}

void
ProjectStore::Oplog::IterateOplog(std::function<void(CbObjectView)>&& Handler)
{
	RwLock::SharedLockScope _(m_OplogLock);
	IterateOplogLocked(std::move(Handler));
}

void
ProjectStore::Oplog::IterateOplogLocked(std::function<void(CbObjectView)>&& Handler)
{
	if (!m_Storage)
	{
		return;
	}

	std::vector<OplogEntryAddress> Entries;
	Entries.reserve(m_LatestOpMap.size());

	for (const auto& Kv : m_LatestOpMap)
	{
		const auto AddressEntry = m_OpAddressMap.find(Kv.second);
		ZEN_ASSERT(AddressEntry != m_OpAddressMap.end());

		Entries.push_back(AddressEntry->second);
	}

	std::sort(Entries.begin(), Entries.end(), [](const OplogEntryAddress& Lhs, const OplogEntryAddress& Rhs) {
		return Lhs.Offset < Rhs.Offset;
	});

	m_Storage->ReplayLogEntries(Entries, [&](CbObjectView Op) { Handler(Op); });
}

size_t
ProjectStore::Oplog::GetOplogEntryCount() const
{
	RwLock::SharedLockScope _(m_OplogLock);
	if (!m_Storage)
	{
		return 0;
	}
	return m_LatestOpMap.size();
}

void
ProjectStore::Oplog::IterateOplogWithKey(std::function<void(int, const Oid&, CbObjectView)>&& Handler)
{
	RwLock::SharedLockScope _(m_OplogLock);
	if (!m_Storage)
	{
		return;
	}

	std::vector<OplogEntryAddress> SortedEntries;
	std::vector<Oid>			   SortedKeys;
	std::vector<int>			   SortedLSNs;

	{
		const auto TargetEntryCount = m_LatestOpMap.size();

		std::vector<size_t>			   EntryIndexes;
		std::vector<OplogEntryAddress> Entries;
		std::vector<Oid>			   Keys;
		std::vector<int>			   LSNs;

		Entries.reserve(TargetEntryCount);
		EntryIndexes.reserve(TargetEntryCount);
		Keys.reserve(TargetEntryCount);
		LSNs.reserve(TargetEntryCount);

		for (const auto& Kv : m_LatestOpMap)
		{
			const auto AddressEntry = m_OpAddressMap.find(Kv.second);
			ZEN_ASSERT(AddressEntry != m_OpAddressMap.end());

			Entries.push_back(AddressEntry->second);
			Keys.push_back(Kv.first);
			LSNs.push_back(Kv.second);
			EntryIndexes.push_back(EntryIndexes.size());
		}

		std::sort(EntryIndexes.begin(), EntryIndexes.end(), [&Entries](const size_t& Lhs, const size_t& Rhs) {
			const OplogEntryAddress& LhsEntry = Entries[Lhs];
			const OplogEntryAddress& RhsEntry = Entries[Rhs];
			return LhsEntry.Offset < RhsEntry.Offset;
		});

		SortedEntries.reserve(EntryIndexes.size());
		SortedKeys.reserve(EntryIndexes.size());
		SortedLSNs.reserve(EntryIndexes.size());

		for (size_t Index : EntryIndexes)
		{
			SortedEntries.push_back(Entries[Index]);
			SortedKeys.push_back(Keys[Index]);
			SortedLSNs.push_back(LSNs[Index]);
		}
	}

	size_t EntryIndex = 0;
	m_Storage->ReplayLogEntries(SortedEntries, [&](CbObjectView Op) {
		Handler(SortedLSNs[EntryIndex], SortedKeys[EntryIndex], Op);
		EntryIndex++;
	});
}

int
ProjectStore::Oplog::GetOpIndexByKey(const Oid& Key)
{
	RwLock::SharedLockScope _(m_OplogLock);
	if (!m_Storage)
	{
		return {};
	}
	if (const auto LatestOp = m_LatestOpMap.find(Key); LatestOp != m_LatestOpMap.end())
	{
		return LatestOp->second;
	}
	return -1;
}

int
ProjectStore::Oplog::GetMaxOpIndex() const
{
	RwLock::SharedLockScope _(m_OplogLock);
	if (!m_Storage)
	{
		return -1;
	}
	return gsl::narrow<int>(m_Storage->GetMaxLsn());
}

std::optional<CbObject>
ProjectStore::Oplog::GetOpByKey(const Oid& Key)
{
	RwLock::SharedLockScope _(m_OplogLock);
	if (!m_Storage)
	{
		return {};
	}

	if (const auto LatestOp = m_LatestOpMap.find(Key); LatestOp != m_LatestOpMap.end())
	{
		const auto AddressEntry = m_OpAddressMap.find(LatestOp->second);
		ZEN_ASSERT(AddressEntry != m_OpAddressMap.end());

		return m_Storage->GetOp(AddressEntry->second);
	}

	return {};
}

std::optional<CbObject>
ProjectStore::Oplog::GetOpByIndex(int Index)
{
	RwLock::SharedLockScope _(m_OplogLock);
	if (!m_Storage)
	{
		return {};
	}

	if (const auto AddressEntryIt = m_OpAddressMap.find(Index); AddressEntryIt != m_OpAddressMap.end())
	{
		return m_Storage->GetOp(AddressEntryIt->second);
	}

	return {};
}

void
ProjectStore::Oplog::AddChunkMappings(const std::unordered_map<Oid, IoHash, Oid::Hasher>& ChunkMappings)
{
	RwLock::ExclusiveLockScope OplogLock(m_OplogLock);
	for (const auto& It : ChunkMappings)
	{
		AddChunkMapping(OplogLock, It.first, It.second);
	}
}

void
ProjectStore::Oplog::CaptureUpdatedLSNs(std::span<const uint32_t> LSNs)
{
	m_UpdateCaptureLock.WithExclusiveLock([&]() {
		if (m_CapturedLSNs)
		{
			m_CapturedLSNs->reserve(m_CapturedLSNs->size() + LSNs.size());
			m_CapturedLSNs->insert(m_CapturedLSNs->end(), LSNs.begin(), LSNs.end());
		}
	});
}

void
ProjectStore::Oplog::CaptureAddedAttachments(std::span<const IoHash> AttachmentHashes)
{
	m_UpdateCaptureLock.WithExclusiveLock([this, AttachmentHashes]() {
		if (m_CapturedAttachments)
		{
			m_CapturedAttachments->reserve(m_CapturedAttachments->size() + AttachmentHashes.size());
			m_CapturedAttachments->insert(m_CapturedAttachments->end(), AttachmentHashes.begin(), AttachmentHashes.end());
		}
	});
}

void
ProjectStore::Oplog::EnableUpdateCapture()
{
	m_UpdateCaptureLock.WithExclusiveLock([&]() {
		if (m_UpdateCaptureRefCounter == 0)
		{
			ZEN_ASSERT(!m_CapturedLSNs);
			ZEN_ASSERT(!m_CapturedAttachments);
			m_CapturedLSNs		  = std::make_unique<std::vector<uint32_t>>();
			m_CapturedAttachments = std::make_unique<std::vector<IoHash>>();
		}
		else
		{
			ZEN_ASSERT(m_CapturedLSNs);
			ZEN_ASSERT(m_CapturedAttachments);
		}
		m_UpdateCaptureRefCounter++;
	});
}

void
ProjectStore::Oplog::DisableUpdateCapture()
{
	m_UpdateCaptureLock.WithExclusiveLock([&]() {
		ZEN_ASSERT(m_CapturedLSNs);
		ZEN_ASSERT(m_CapturedAttachments);
		ZEN_ASSERT(m_UpdateCaptureRefCounter > 0);
		m_UpdateCaptureRefCounter--;
		if (m_UpdateCaptureRefCounter == 0)
		{
			m_CapturedLSNs.reset();
			m_CapturedAttachments.reset();
		}
	});
}

void
ProjectStore::Oplog::IterateCapturedLSNs(std::function<bool(const CbObjectView& UpdateOp)>&& Callback)
{
	m_UpdateCaptureLock.WithExclusiveLock([&]() {
		if (m_CapturedLSNs)
		{
			if (!m_Storage)
			{
				return;
			}
			for (int UpdatedLSN : *m_CapturedLSNs)
			{
				if (const auto AddressEntryIt = m_OpAddressMap.find(UpdatedLSN); AddressEntryIt != m_OpAddressMap.end())
				{
					Callback(m_Storage->GetOp(AddressEntryIt->second));
				}
			}
		}
	});
}

std::vector<IoHash>
ProjectStore::Oplog::GetCapturedAttachments()
{
	RwLock::SharedLockScope _(m_UpdateCaptureLock);
	if (m_CapturedAttachments)
	{
		return *m_CapturedAttachments;
	}
	return {};
}

void
ProjectStore::Oplog::AddFileMapping(const RwLock::ExclusiveLockScope&,
									const Oid&		 FileId,
									const IoHash&	 Hash,
									std::string_view ServerPath,
									std::string_view ClientPath)
{
	FileMapEntry Entry;

	if (Hash != IoHash::Zero)
	{
		m_ChunkMap.insert_or_assign(FileId, Hash);
	}
	else
	{
		Entry.ServerPath = ServerPath;
	}

	Entry.ClientPath = ClientPath;

	m_FileMap[FileId] = std::move(Entry);
}

void
ProjectStore::Oplog::AddChunkMapping(const RwLock::ExclusiveLockScope&, const Oid& ChunkId, const IoHash& Hash)
{
	m_ChunkMap.insert_or_assign(ChunkId, Hash);
}

void
ProjectStore::Oplog::AddMetaMapping(const RwLock::ExclusiveLockScope&, const Oid& ChunkId, const IoHash& Hash)
{
	m_MetaMap.insert_or_assign(ChunkId, Hash);
}

ProjectStore::Oplog::OplogEntryMapping
ProjectStore::Oplog::GetMapping(CbObjectView Core)
{
	using namespace std::literals;

	OplogEntryMapping Result;

	// Update chunk id maps
	for (CbFieldView Field : Core)
	{
		std::string_view FieldName = Field.GetName();
		if (FieldName == "package"sv)
		{
			CbObjectView PackageObj = Field.AsObjectView();
			Oid			 Id			= PackageObj["id"sv].AsObjectId();
			IoHash		 Hash		= PackageObj["data"sv].AsBinaryAttachment();
			Result.Chunks.emplace_back(OplogEntryMapping::Mapping{Id, Hash});
			ZEN_DEBUG("package data {} -> {}", Id, Hash);
			continue;
		}
		if (FieldName == "bulkdata"sv)
		{
			CbArrayView BulkDataArray = Field.AsArrayView();
			for (CbFieldView& Entry : BulkDataArray)
			{
				CbObjectView BulkObj = Entry.AsObjectView();
				Oid			 Id		 = BulkObj["id"sv].AsObjectId();
				IoHash		 Hash	 = BulkObj["data"sv].AsBinaryAttachment();
				Result.Chunks.emplace_back(OplogEntryMapping::Mapping{Id, Hash});
				ZEN_DEBUG("bulkdata {} -> {}", Id, Hash);
			}
			continue;
		}
		if (FieldName == "packagedata"sv)
		{
			CbArrayView PackageDataArray = Field.AsArrayView();
			for (CbFieldView& Entry : PackageDataArray)
			{
				CbObjectView PackageDataObj = Entry.AsObjectView();
				Oid			 Id				= PackageDataObj["id"sv].AsObjectId();
				IoHash		 Hash			= PackageDataObj["data"sv].AsBinaryAttachment();
				Result.Chunks.emplace_back(OplogEntryMapping::Mapping{Id, Hash});
				ZEN_DEBUG("package {} -> {}", Id, Hash);
			}
			continue;
		}
		if (FieldName == "files"sv)
		{
			CbArrayView FilesArray = Field.AsArrayView();
			Result.Files.reserve(FilesArray.Num());
			for (CbFieldView& Entry : FilesArray)
			{
				CbObjectView FileObj = Entry.AsObjectView();

				std::string_view ServerPath = FileObj["serverpath"sv].AsString();
				std::string_view ClientPath = FileObj["clientpath"sv].AsString();
				Oid				 Id			= FileObj["id"sv].AsObjectId();
				IoHash			 Hash		= FileObj["data"sv].AsBinaryAttachment();
				if (ServerPath.empty() && Hash == IoHash::Zero)
				{
					ZEN_WARN("invalid file for entry '{}', missing both 'serverpath' and 'data' fields", Id);
					continue;
				}
				if (ClientPath.empty())
				{
					ZEN_WARN("invalid file for entry '{}', missing 'clientpath' field", Id);
					continue;
				}

				Result.Files.emplace_back(OplogEntryMapping::FileMapping{Id, Hash, std::string(ServerPath), std::string(ClientPath)});
				ZEN_DEBUG("file {} -> {}, ServerPath: {}, ClientPath: {}", Id, Hash, ServerPath, ClientPath);
			}
			continue;
		}
		if (FieldName == "meta"sv)
		{
			CbArrayView MetaArray = Field.AsArrayView();
			Result.Meta.reserve(MetaArray.Num());
			for (CbFieldView& Entry : MetaArray)
			{
				CbObjectView MetaObj = Entry.AsObjectView();
				Oid			 Id		 = MetaObj["id"sv].AsObjectId();
				IoHash		 Hash	 = MetaObj["data"sv].AsBinaryAttachment();
				Result.Meta.emplace_back(OplogEntryMapping::Mapping{Id, Hash});
				auto NameString = MetaObj["name"sv].AsString();
				ZEN_DEBUG("meta data ({}) {} -> {}", NameString, Id, Hash);
			}
			continue;
		}
	}

	return Result;
}

uint32_t
ProjectStore::Oplog::RegisterOplogEntry(RwLock::ExclusiveLockScope& OplogLock,
										const OplogEntryMapping&	OpMapping,
										const OplogEntry&			OpEntry)
{
	// For now we're assuming the update is all in-memory so we can hold an exclusive lock without causing
	// too many problems. Longer term we'll probably want to ensure we can do concurrent updates however

	using namespace std::literals;

	// Update chunk id maps
	for (const OplogEntryMapping::Mapping& Chunk : OpMapping.Chunks)
	{
		AddChunkMapping(OplogLock, Chunk.Id, Chunk.Hash);
	}

	for (const OplogEntryMapping::FileMapping& File : OpMapping.Files)
	{
		AddFileMapping(OplogLock, File.Id, File.Hash, File.ServerPath, File.ClientPath);
	}

	for (const OplogEntryMapping::Mapping& Meta : OpMapping.Meta)
	{
		AddMetaMapping(OplogLock, Meta.Id, Meta.Hash);
	}

	m_OpAddressMap.emplace(OpEntry.OpLsn, OplogEntryAddress{.Offset = OpEntry.OpCoreOffset, .Size = OpEntry.OpCoreSize});
	m_LatestOpMap[OpEntry.OpKeyHash] = OpEntry.OpLsn;
	return OpEntry.OpLsn;
}

uint32_t
ProjectStore::Oplog::AppendNewOplogEntry(CbPackage OpPackage)
{
	ZEN_TRACE_CPU("Store::Oplog::AppendNewOplogEntry");

	const CbObject& Core	= OpPackage.GetObject();
	const uint32_t	EntryId = AppendNewOplogEntry(Core);
	if (EntryId == 0xffffffffu)
	{
		// The oplog has been deleted so just drop this
		return EntryId;
	}

	// Persist attachments after oplog entry so GC won't find attachments without references

	uint64_t AttachmentBytes	= 0;
	uint64_t NewAttachmentBytes = 0;

	auto Attachments = OpPackage.GetAttachments();

	if (!Attachments.empty())
	{
		std::vector<IoBuffer> WriteAttachmentBuffers;
		std::vector<IoHash>	  WriteRawHashes;
		std::vector<uint64_t> WriteRawSizes;

		WriteAttachmentBuffers.reserve(Attachments.size());
		WriteRawHashes.reserve(Attachments.size());
		WriteRawSizes.reserve(Attachments.size());

		for (const auto& Attach : Attachments)
		{
			ZEN_ASSERT(Attach.IsCompressedBinary());

			const CompressedBuffer& AttachmentData = Attach.AsCompressedBinary();
			const uint64_t			AttachmentSize = AttachmentData.DecodeRawSize();
			WriteAttachmentBuffers.push_back(AttachmentData.GetCompressed().Flatten().AsIoBuffer());
			WriteRawHashes.push_back(Attach.GetHash());
			WriteRawSizes.push_back(AttachmentSize);
			AttachmentBytes += AttachmentSize;
		}

		std::vector<CidStore::InsertResult> InsertResults = m_CidStore.AddChunks(WriteAttachmentBuffers, WriteRawHashes);
		for (size_t Index = 0; Index < InsertResults.size(); Index++)
		{
			if (InsertResults[Index].New)
			{
				NewAttachmentBytes += WriteRawSizes[Index];
			}
		}
	}

	ZEN_DEBUG("oplog entry #{} attachments: {} new, {} total", EntryId, NiceBytes(NewAttachmentBytes), NiceBytes(AttachmentBytes));

	return EntryId;
}

RefPtr<ProjectStore::OplogStorage>
ProjectStore::Oplog::GetStorage()
{
	RefPtr<OplogStorage> Storage;
	{
		RwLock::SharedLockScope _(m_OplogLock);
		Storage = m_Storage;
	}
	return Storage;
}

uint32_t
ProjectStore::Oplog::AppendNewOplogEntry(CbObjectView Core)
{
	ZEN_TRACE_CPU("Store::Oplog::AppendNewOplogEntry");

	using namespace std::literals;

	RefPtr<OplogStorage> Storage = GetStorage();
	if (!m_Storage)
	{
		return 0xffffffffu;
	}

	OplogEntryMapping		   Mapping = GetMapping(Core);
	OplogStorage::AppendOpData OpData  = OplogStorage::GetAppendOpData(Core);

	const OplogEntry OpEntry = m_Storage->AppendOp(OpData);

	RwLock::ExclusiveLockScope OplogLock(m_OplogLock);
	const uint32_t			   EntryId = RegisterOplogEntry(OplogLock, Mapping, OpEntry);
	CaptureUpdatedLSNs(std::array<uint32_t, 1u>({EntryId}));

	return EntryId;
}

std::vector<uint32_t>
ProjectStore::Oplog::AppendNewOplogEntries(std::span<CbObjectView> Cores)
{
	ZEN_TRACE_CPU("Store::Oplog::AppendNewOplogEntries");

	using namespace std::literals;

	RefPtr<OplogStorage> Storage = GetStorage();
	if (!m_Storage)
	{
		return std::vector<uint32_t>(Cores.size(), 0xffffffffu);
	}

	size_t									OpCount = Cores.size();
	std::vector<OplogEntryMapping>			Mappings;
	std::vector<OplogStorage::AppendOpData> OpDatas;
	Mappings.resize(OpCount);
	OpDatas.resize(OpCount);

	for (size_t OpIndex = 0; OpIndex < OpCount; OpIndex++)
	{
		const CbObjectView& Core = Cores[OpIndex];
		OpDatas[OpIndex]		 = OplogStorage::GetAppendOpData(Core);
		Mappings[OpIndex]		 = GetMapping(Core);
	}

	std::vector<OplogEntry> OpEntries = Storage->AppendOps(OpDatas);

	std::vector<uint32_t> EntryIds;
	EntryIds.resize(OpCount);
	{
		RwLock::ExclusiveLockScope OplogLock(m_OplogLock);
		for (size_t OpIndex = 0; OpIndex < OpCount; OpIndex++)
		{
			EntryIds[OpIndex] = RegisterOplogEntry(OplogLock, Mappings[OpIndex], OpEntries[OpIndex]);
		}
		CaptureUpdatedLSNs(EntryIds);
	}
	return EntryIds;
}

//////////////////////////////////////////////////////////////////////////

ProjectStore::Project::Project(ProjectStore* PrjStore, CidStore& Store, std::filesystem::path BasePath)
: m_ProjectStore(PrjStore)
, m_CidStore(Store)
, m_OplogStoragePath(BasePath)
, m_LastAccessTimes({std::make_pair(std::string(), GcClock::TickCount())})
{
}

ProjectStore::Project::~Project()
{
	// Only write access times if we have not been explicitly deleted
	if (!m_OplogStoragePath.empty())
	{
		WriteAccessTimes();
	}
}

bool
ProjectStore::Project::Exists(const std::filesystem::path& BasePath)
{
	return std::filesystem::exists(BasePath / "Project.zcb");
}

void
ProjectStore::Project::Read()
{
	ZEN_TRACE_CPU("Store::Project::Read");

	using namespace std::literals;

	std::filesystem::path ProjectStateFilePath = m_OplogStoragePath / "Project.zcb"sv;

	ZEN_INFO("reading config for project '{}' from {}", Identifier, ProjectStateFilePath);

	BasicFile Blob;
	Blob.Open(ProjectStateFilePath, BasicFile::Mode::kRead);

	IoBuffer		Obj				= Blob.ReadAll();
	CbValidateError ValidationError = ValidateCompactBinary(MemoryView(Obj.Data(), Obj.Size()), CbValidateMode::All);

	if (ValidationError == CbValidateError::None)
	{
		CbObject Cfg = LoadCompactBinaryObject(Obj);

		Identifier		= std::filesystem::path(Cfg["id"sv].AsU8String()).string();
		RootDir			= std::filesystem::path(Cfg["root"sv].AsU8String()).string();
		ProjectRootDir	= std::filesystem::path(Cfg["project"sv].AsU8String()).string();
		EngineRootDir	= std::filesystem::path(Cfg["engine"sv].AsU8String()).string();
		ProjectFilePath = std::filesystem::path(Cfg["projectfile"sv].AsU8String()).string();
	}
	else
	{
		ZEN_ERROR("validation error {} hit for '{}'", int(ValidationError), ProjectStateFilePath);
	}

	ReadAccessTimes();
}

void
ProjectStore::Project::Write()
{
	ZEN_TRACE_CPU("Store::Project::Write");

	using namespace std::literals;

	BinaryWriter Mem;

	CbObjectWriter Cfg;
	Cfg << "id"sv << Identifier;
	Cfg << "root"sv << PathToUtf8(RootDir);
	Cfg << "project"sv << PathToUtf8(ProjectRootDir);
	Cfg << "engine"sv << PathToUtf8(EngineRootDir);
	Cfg << "projectfile"sv << PathToUtf8(ProjectFilePath);

	Cfg.Save(Mem);

	CreateDirectories(m_OplogStoragePath);

	std::filesystem::path ProjectStateFilePath = m_OplogStoragePath / "Project.zcb"sv;

	ZEN_INFO("persisting config for project '{}' to {}", Identifier, ProjectStateFilePath);

	TemporaryFile::SafeWriteFile(ProjectStateFilePath, Mem.GetView());
}

void
ProjectStore::Project::ReadAccessTimes()
{
	using namespace std::literals;

	RwLock::SharedLockScope _(m_ProjectLock);

	std::filesystem::path ProjectAccessTimesFilePath = m_OplogStoragePath / "AccessTimes.zcb"sv;
	if (!std::filesystem::exists(ProjectAccessTimesFilePath))
	{
		return;
	}

	ZEN_INFO("reading access times for project '{}' from {}", Identifier, ProjectAccessTimesFilePath);

	BasicFile Blob;
	Blob.Open(ProjectAccessTimesFilePath, BasicFile::Mode::kRead);

	IoBuffer		Obj				= Blob.ReadAll();
	CbValidateError ValidationError = ValidateCompactBinary(MemoryView(Obj.Data(), Obj.Size()), CbValidateMode::All);

	if (ValidationError == CbValidateError::None)
	{
		CbObject Reader = LoadCompactBinaryObject(Obj);

		uint64_t Count = Reader["count"sv].AsUInt64(0);
		if (Count > 0)
		{
			std::vector<uint64_t> Ticks;
			Ticks.reserve(Count);
			CbArrayView TicksArray = Reader["ticks"sv].AsArrayView();
			for (CbFieldView& TickView : TicksArray)
			{
				Ticks.emplace_back(TickView.AsUInt64());
			}
			CbArrayView IdArray = Reader["ids"sv].AsArrayView();
			uint64_t	Index	= 0;
			for (CbFieldView& IdView : IdArray)
			{
				std::string_view Id = IdView.AsString();
				m_LastAccessTimes.insert_or_assign(std::string(Id), Ticks[Index++]);
			}
		}

		////// Legacy format read
		{
			CbArrayView LastAccessTimes = Reader["lastaccess"sv].AsArrayView();
			for (CbFieldView& Entry : LastAccessTimes)
			{
				CbObjectView	 AccessTime = Entry.AsObjectView();
				std::string_view Id			= AccessTime["id"sv].AsString();
				GcClock::Tick	 AccessTick = AccessTime["tick"sv].AsUInt64();
				m_LastAccessTimes.insert_or_assign(std::string(Id), AccessTick);
			}
		}
	}
	else
	{
		ZEN_ERROR("validation error {} hit for '{}'", int(ValidationError), ProjectAccessTimesFilePath);
	}
}

void
ProjectStore::Project::WriteAccessTimes()
{
	using namespace std::literals;

	CbObjectWriter Writer;

	Writer.AddInteger("count", gsl::narrow<uint64_t>(m_LastAccessTimes.size()));
	Writer.BeginArray("ids");

	{
		RwLock::SharedLockScope _(m_ProjectLock);
		for (const auto& It : m_LastAccessTimes)
		{
			Writer << It.first;
		}
		Writer.EndArray();
		Writer.BeginArray("ticks");
		for (const auto& It : m_LastAccessTimes)
		{
			Writer << gsl::narrow<uint64_t>(It.second);
		}
		Writer.EndArray();
	}

	CbObject Data = Writer.Save();

	try
	{
		CreateDirectories(m_OplogStoragePath);

		std::filesystem::path ProjectAccessTimesFilePath = m_OplogStoragePath / "AccessTimes.zcb"sv;

		ZEN_INFO("persisting access times for project '{}' to {}", Identifier, ProjectAccessTimesFilePath);

		TemporaryFile::SafeWriteFile(ProjectAccessTimesFilePath, Data.GetView());
	}
	catch (const std::exception& Err)
	{
		ZEN_WARN("writing access times FAILED, reason: '{}'", Err.what());
	}
}

LoggerRef
ProjectStore::Project::Log()
{
	return m_ProjectStore->Log();
}

std::filesystem::path
ProjectStore::Project::BasePathForOplog(std::string_view OplogId)
{
	return m_OplogStoragePath / OplogId;
}

ProjectStore::Oplog*
ProjectStore::Project::NewOplog(std::string_view OplogId, const std::filesystem::path& MarkerPath)
{
	RwLock::ExclusiveLockScope _(m_ProjectLock);

	std::filesystem::path OplogBasePath = BasePathForOplog(OplogId);

	try
	{
		Oplog* Log = m_Oplogs
						 .try_emplace(std::string{OplogId},
									  std::make_unique<ProjectStore::Oplog>(OplogId, this, m_CidStore, OplogBasePath, MarkerPath))
						 .first->second.get();

		Log->Write();

		m_UpdateCaptureLock.WithExclusiveLock([&]() {
			if (m_CapturedOplogs)
			{
				m_CapturedOplogs->push_back(std::string(OplogId));
			}
		});

		return Log;
	}
	catch (const std::exception&)
	{
		// In case of failure we need to ensure there's no half constructed entry around
		//
		// (This is probably already ensured by the try_emplace implementation?)

		m_Oplogs.erase(std::string{OplogId});

		return nullptr;
	}
}

ProjectStore::Oplog*
ProjectStore::Project::OpenOplog(std::string_view OplogId)
{
	ZEN_TRACE_CPU("Store::OpenOplog");
	{
		RwLock::SharedLockScope _(m_ProjectLock);

		auto OplogIt = m_Oplogs.find(std::string(OplogId));

		if (OplogIt != m_Oplogs.end())
		{
			return OplogIt->second.get();
		}
	}

	RwLock::ExclusiveLockScope _(m_ProjectLock);

	std::filesystem::path OplogBasePath = BasePathForOplog(OplogId);

	if (Oplog::ExistsAt(OplogBasePath))
	{
		// Do open of existing oplog

		try
		{
			Oplog* Log =
				m_Oplogs
					.try_emplace(std::string{OplogId},
								 std::make_unique<ProjectStore::Oplog>(OplogId, this, m_CidStore, OplogBasePath, std::filesystem::path{}))
					.first->second.get();
			Log->Read();

			return Log;
		}
		catch (const std::exception& ex)
		{
			ZEN_WARN("failed to open oplog '{}' @ '{}': {}", OplogId, OplogBasePath, ex.what());

			m_Oplogs.erase(std::string{OplogId});
		}
	}

	return nullptr;
}

std::filesystem::path
ProjectStore::Project::RemoveOplog(std::string_view OplogId)
{
	RwLock::ExclusiveLockScope _(m_ProjectLock);

	std::filesystem::path DeletePath;
	if (auto OplogIt = m_Oplogs.find(std::string(OplogId)); OplogIt == m_Oplogs.end())
	{
		std::filesystem::path OplogBasePath = BasePathForOplog(OplogId);

		if (Oplog::ExistsAt(OplogBasePath))
		{
			std::filesystem::path MovedDir;
			if (PrepareDirectoryDelete(DeletePath, MovedDir))
			{
				DeletePath = MovedDir;
			}
		}
	}
	else
	{
		std::unique_ptr<Oplog>& Oplog = OplogIt->second;
		DeletePath					  = Oplog->PrepareForDelete(true);
		m_DeletedOplogs.emplace_back(std::move(Oplog));
		m_Oplogs.erase(OplogIt);
	}
	m_LastAccessTimes.erase(std::string(OplogId));
	return DeletePath;
}

void
ProjectStore::Project::DeleteOplog(std::string_view OplogId)
{
	std::filesystem::path DeletePath = RemoveOplog(OplogId);

	// Erase content on disk
	if (!DeletePath.empty())
	{
		OplogStorage::Delete(DeletePath);
	}
}

std::vector<std::string>
ProjectStore::Project::ScanForOplogs() const
{
	DirectoryContent DirContent;
	GetDirectoryContent(m_OplogStoragePath, DirectoryContent::IncludeDirsFlag, DirContent);
	std::vector<std::string> Oplogs;
	Oplogs.reserve(DirContent.Directories.size());
	for (const std::filesystem::path& DirPath : DirContent.Directories)
	{
		Oplogs.push_back(DirPath.filename().string());
	}
	return Oplogs;
}

void
ProjectStore::Project::IterateOplogs(std::function<void(const RwLock::SharedLockScope&, const Oplog&)>&& Fn) const
{
	RwLock::SharedLockScope Lock(m_ProjectLock);

	for (auto& Kv : m_Oplogs)
	{
		Fn(Lock, *Kv.second);
	}
}

void
ProjectStore::Project::IterateOplogs(std::function<void(const RwLock::SharedLockScope&, Oplog&)>&& Fn)
{
	RwLock::SharedLockScope Lock(m_ProjectLock);

	for (auto& Kv : m_Oplogs)
	{
		Fn(Lock, *Kv.second);
	}
}

void
ProjectStore::Project::Flush()
{
	// We only need to flush oplogs that we have already loaded
	IterateOplogs([&](const RwLock::SharedLockScope&, Oplog& Ops) { Ops.Flush(); });
	WriteAccessTimes();
}

void
ProjectStore::Project::ScrubStorage(ScrubContext& Ctx)
{
	// Scrubbing needs to check all existing oplogs
	std::vector<std::string> OpLogs = ScanForOplogs();
	for (const std::string& OpLogId : OpLogs)
	{
		OpenOplog(OpLogId);
	}
	IterateOplogs([&](const RwLock::SharedLockScope& ProjectLock, Oplog& Ops) {
		if (!IsExpired(ProjectLock, GcClock::TimePoint::min(), Ops))
		{
			Ops.ScrubStorage(Ctx);
		}
	});
}

void
ProjectStore::Project::GatherReferences(GcContext& GcCtx)
{
	ZEN_TRACE_CPU("Store::Project::GatherReferences");

	Stopwatch  Timer;
	const auto Guard = MakeGuard([&] {
		ZEN_DEBUG("gathered references from project store project {} in {}", Identifier, NiceTimeSpanMs(Timer.GetElapsedTimeMs()));
	});

	// GatherReferences needs to check all existing oplogs
	std::vector<std::string> OpLogs = ScanForOplogs();
	for (const std::string& OpLogId : OpLogs)
	{
		OpenOplog(OpLogId);
	}

	{
		// Make sure any oplog at least have a last access time so they eventually will be GC:d if not touched
		RwLock::ExclusiveLockScope _(m_ProjectLock);
		for (const std::string& OpId : OpLogs)
		{
			if (auto It = m_LastAccessTimes.find(OpId); It == m_LastAccessTimes.end())
			{
				m_LastAccessTimes[OpId] = GcClock::TickCount();
			}
		}
	}

	IterateOplogs([&](const RwLock::SharedLockScope& ProjectLock, Oplog& Ops) {
		if (!IsExpired(ProjectLock, GcCtx.ProjectStoreExpireTime(), Ops))
		{
			Ops.GatherReferences(GcCtx);
		}
	});
}

uint64_t
ProjectStore::Project::TotalSize(const std::filesystem::path& BasePath)
{
	using namespace std::literals;

	uint64_t			  Size				  = 0;
	std::filesystem::path AccessTimesFilePath = BasePath / "AccessTimes.zcb"sv;
	if (std::filesystem::exists(AccessTimesFilePath))
	{
		Size += std::filesystem::file_size(AccessTimesFilePath);
	}
	std::filesystem::path ProjectFilePath = BasePath / "Project.zcb"sv;
	if (std::filesystem::exists(ProjectFilePath))
	{
		Size += std::filesystem::file_size(ProjectFilePath);
	}

	return Size;
}

uint64_t
ProjectStore::Project::TotalSize() const
{
	uint64_t Result = TotalSize(m_OplogStoragePath);
	{
		std::vector<std::string> OpLogs = ScanForOplogs();
		for (const std::string& OpLogId : OpLogs)
		{
			std::filesystem::path OplogBasePath = m_OplogStoragePath / OpLogId;
			Result += Oplog::TotalSize(OplogBasePath);
		}
	}
	return Result;
}

bool
ProjectStore::Project::PrepareForDelete(std::filesystem::path& OutDeletePath)
{
	RwLock::ExclusiveLockScope _(m_ProjectLock);

	for (auto& It : m_Oplogs)
	{
		// We don't care about the moved folder
		It.second->PrepareForDelete(false);
		m_DeletedOplogs.emplace_back(std::move(It.second));
	}

	m_Oplogs.clear();

	bool Success = PrepareDirectoryDelete(m_OplogStoragePath, OutDeletePath);
	if (!Success)
	{
		return false;
	}
	m_OplogStoragePath.clear();
	return true;
}

void
ProjectStore::Project::EnableUpdateCapture()
{
	m_UpdateCaptureLock.WithExclusiveLock([&]() {
		if (m_UpdateCaptureRefCounter == 0)
		{
			ZEN_ASSERT(!m_CapturedOplogs);
			m_CapturedOplogs = std::make_unique<std::vector<std::string>>();
		}
		else
		{
			ZEN_ASSERT(m_CapturedOplogs);
		}
		m_UpdateCaptureRefCounter++;
	});
}

void
ProjectStore::Project::DisableUpdateCapture()
{
	m_UpdateCaptureLock.WithExclusiveLock([&]() {
		ZEN_ASSERT(m_CapturedOplogs);
		ZEN_ASSERT(m_UpdateCaptureRefCounter > 0);
		m_UpdateCaptureRefCounter--;
		if (m_UpdateCaptureRefCounter == 0)
		{
			m_CapturedOplogs.reset();
		}
	});
}

std::vector<std::string>
ProjectStore::Project::GetCapturedOplogs()
{
	RwLock::SharedLockScope _(m_UpdateCaptureLock);
	if (m_CapturedOplogs)
	{
		return *m_CapturedOplogs;
	}
	return {};
}

std::vector<RwLock::SharedLockScope>
ProjectStore::Project::GetGcReferencerLocks()
{
	std::vector<RwLock::SharedLockScope> Locks;
	Locks.emplace_back(RwLock::SharedLockScope(m_ProjectLock));
	Locks.reserve(1 + m_Oplogs.size());
	for (auto& Kv : m_Oplogs)
	{
		Locks.emplace_back(Kv.second->GetGcReferencerLock());
	}
	return Locks;
}

bool
ProjectStore::Project::IsExpired(const RwLock::SharedLockScope&,
								 const std::string&			  EntryName,
								 const std::filesystem::path& MarkerPath,
								 const GcClock::TimePoint	  ExpireTime)
{
	if (!MarkerPath.empty())
	{
		std::error_code Ec;
		if (std::filesystem::exists(MarkerPath, Ec))
		{
			if (Ec)
			{
				ZEN_WARN("Failed to check expiry via marker file '{}', assuming {} is not expired",
						 EntryName.empty() ? "project" : EntryName,
						 MarkerPath.string());
				return false;
			}
			return false;
		}
	}

	const GcClock::Tick ExpireTicks = ExpireTime.time_since_epoch().count();

	if (auto It = m_LastAccessTimes.find(EntryName); It != m_LastAccessTimes.end())
	{
		if (It->second <= ExpireTicks)
		{
			return true;
		}
	}
	return false;
}

bool
ProjectStore::Project::IsExpired(const RwLock::SharedLockScope& ProjectLock, const GcClock::TimePoint ExpireTime)
{
	return IsExpired(ProjectLock, std::string(), ProjectFilePath, ExpireTime);
}

bool
ProjectStore::Project::IsExpired(const RwLock::SharedLockScope& ProjectLock,
								 const GcClock::TimePoint		ExpireTime,
								 const ProjectStore::Oplog&		Oplog)
{
	return IsExpired(ProjectLock, Oplog.OplogId(), Oplog.MarkerPath(), ExpireTime);
}

bool
ProjectStore::Project::IsExpired(const GcClock::TimePoint ExpireTime, const ProjectStore::Oplog& Oplog)
{
	RwLock::SharedLockScope Lock(m_ProjectLock);
	return IsExpired(Lock, Oplog.OplogId(), Oplog.MarkerPath(), ExpireTime);
}

void
ProjectStore::Project::TouchProject() const
{
	RwLock::ExclusiveLockScope _(m_ProjectLock);
	m_LastAccessTimes.insert_or_assign(std::string(), GcClock::TickCount());
};

void
ProjectStore::Project::TouchOplog(std::string_view Oplog) const
{
	ZEN_ASSERT(!Oplog.empty());
	RwLock::ExclusiveLockScope _(m_ProjectLock);
	m_LastAccessTimes.insert_or_assign(std::string(Oplog), GcClock::TickCount());
};

GcClock::TimePoint
ProjectStore::Project::LastOplogAccessTime(std::string_view Oplog) const
{
	RwLock::SharedLockScope Lock(m_ProjectLock);
	if (auto It = m_LastAccessTimes.find(std::string(Oplog)); It != m_LastAccessTimes.end())
	{
		return GcClock::TimePointFromTick(It->second);
	}
	return GcClock::TimePoint::min();
}

//////////////////////////////////////////////////////////////////////////

ProjectStore::ProjectStore(CidStore& Store, std::filesystem::path BasePath, GcManager& Gc, JobQueue& JobQueue)
: m_Log(logging::Get("project"))
, m_Gc(Gc)
, m_CidStore(Store)
, m_JobQueue(JobQueue)
, m_ProjectBasePath(BasePath)
, m_DiskWriteBlocker(Gc.GetDiskWriteBlocker())
{
	ZEN_INFO("initializing project store at '{}'", m_ProjectBasePath);
	// m_Log.set_level(spdlog::level::debug);
	m_Gc.AddGcContributor(this);
	m_Gc.AddGcStorage(this);
	m_Gc.AddGcReferencer(*this);
	m_Gc.AddGcReferenceLocker(*this);
}

ProjectStore::~ProjectStore()
{
	ZEN_INFO("closing project store at '{}'", m_ProjectBasePath);
	m_Gc.RemoveGcReferenceLocker(*this);
	m_Gc.RemoveGcReferencer(*this);
	m_Gc.RemoveGcStorage(this);
	m_Gc.RemoveGcContributor(this);
}

std::filesystem::path
ProjectStore::BasePathForProject(std::string_view ProjectId)
{
	return m_ProjectBasePath / ProjectId;
}

void
ProjectStore::DiscoverProjects()
{
	if (!std::filesystem::exists(m_ProjectBasePath))
	{
		return;
	}

	DirectoryContent DirContent;
	GetDirectoryContent(m_ProjectBasePath, DirectoryContent::IncludeDirsFlag, DirContent);

	for (const std::filesystem::path& DirPath : DirContent.Directories)
	{
		std::string DirName = PathToUtf8(DirPath.filename());
		if (DirName.starts_with("[dropped]"))
		{
			continue;
		}
		OpenProject(DirName);
	}
}

void
ProjectStore::IterateProjects(std::function<void(Project& Prj)>&& Fn)
{
	RwLock::SharedLockScope _(m_ProjectsLock);

	for (auto& Kv : m_Projects)
	{
		Fn(*Kv.second.Get());
	}
}

void
ProjectStore::Flush()
{
	ZEN_INFO("flushing project store at '{}'", m_ProjectBasePath);
	std::vector<Ref<Project>> Projects;
	{
		RwLock::SharedLockScope _(m_ProjectsLock);
		Projects.reserve(m_Projects.size());

		for (auto& Kv : m_Projects)
		{
			Projects.push_back(Kv.second);
		}
	}
	for (const Ref<Project>& Project : Projects)
	{
		Project->Flush();
	}
}

void
ProjectStore::ScrubStorage(ScrubContext& Ctx)
{
	ZEN_INFO("scrubbing '{}'", m_ProjectBasePath);

	DiscoverProjects();

	std::vector<Ref<Project>> Projects;
	{
		RwLock::SharedLockScope Lock(m_ProjectsLock);
		Projects.reserve(m_Projects.size());

		for (auto& Kv : m_Projects)
		{
			if (Kv.second->IsExpired(Lock, GcClock::TimePoint::min()))
			{
				continue;
			}
			Projects.push_back(Kv.second);
		}
	}
	for (const Ref<Project>& Project : Projects)
	{
		Project->ScrubStorage(Ctx);
	}
}

void
ProjectStore::GatherReferences(GcContext& GcCtx)
{
	ZEN_TRACE_CPU("Store::GatherReferences");

	size_t	   ProjectCount		   = 0;
	size_t	   ExpiredProjectCount = 0;
	Stopwatch  Timer;
	const auto Guard = MakeGuard([&] {
		ZEN_DEBUG("gathered references from '{}' in {}, found {} active projects and {} expired projects",
				  m_ProjectBasePath.string(),
				  NiceTimeSpanMs(Timer.GetElapsedTimeMs()),
				  ProjectCount,
				  ExpiredProjectCount);
	});

	DiscoverProjects();

	std::vector<Ref<Project>> Projects;
	{
		RwLock::SharedLockScope Lock(m_ProjectsLock);
		Projects.reserve(m_Projects.size());

		for (auto& Kv : m_Projects)
		{
			if (Kv.second->IsExpired(Lock, GcCtx.ProjectStoreExpireTime()))
			{
				ExpiredProjectCount++;
				continue;
			}
			Projects.push_back(Kv.second);
		}
	}
	ProjectCount = Projects.size();
	for (const Ref<Project>& Project : Projects)
	{
		Project->GatherReferences(GcCtx);
	}
}

void
ProjectStore::CollectGarbage(GcContext& GcCtx)
{
	ZEN_TRACE_CPU("Store::CollectGarbage");

	size_t ProjectCount		   = 0;
	size_t ExpiredProjectCount = 0;

	Stopwatch				  Timer;
	const auto				  Guard = MakeGuard([&] {
		   ZEN_DEBUG("garbage collect from '{}' DONE after {}, found {} active projects and {} expired projects",
					 m_ProjectBasePath.string(),
					 NiceTimeSpanMs(Timer.GetElapsedTimeMs()),
					 ProjectCount,
					 ExpiredProjectCount);
	   });
	std::vector<Ref<Project>> ExpiredProjects;
	std::vector<Ref<Project>> Projects;

	{
		RwLock::SharedLockScope Lock(m_ProjectsLock);
		for (auto& Kv : m_Projects)
		{
			if (Kv.second->IsExpired(Lock, GcCtx.ProjectStoreExpireTime()))
			{
				ExpiredProjects.push_back(Kv.second);
				ExpiredProjectCount++;
				continue;
			}
			Projects.push_back(Kv.second);
			ProjectCount++;
		}
	}

	if (!GcCtx.IsDeletionMode())
	{
		ZEN_DEBUG("garbage collect DISABLED, for '{}' ", m_ProjectBasePath.string());
		return;
	}

	for (const Ref<Project>& Project : Projects)
	{
		std::vector<std::string> ExpiredOplogs;
		{
			RwLock::ExclusiveLockScope _(m_ProjectsLock);
			Project->IterateOplogs([&GcCtx, &Project, &ExpiredOplogs](const RwLock::SharedLockScope& Lock, ProjectStore::Oplog& Oplog) {
				if (Project->IsExpired(Lock, GcCtx.ProjectStoreExpireTime(), Oplog))
				{
					ExpiredOplogs.push_back(Oplog.OplogId());
				}
			});
		}
		for (const std::string& OplogId : ExpiredOplogs)
		{
			ZEN_DEBUG("ProjectStore::CollectGarbage garbage collected oplog '{}' in project '{}'. Removing storage on disk",
					  OplogId,
					  Project->Identifier);
			Project->DeleteOplog(OplogId);
		}
		Project->Flush();
	}

	if (ExpiredProjects.empty())
	{
		ZEN_DEBUG("garbage collect for '{}', no expired projects found", m_ProjectBasePath.string());
		return;
	}

	for (const Ref<Project>& Project : ExpiredProjects)
	{
		std::filesystem::path PathToRemove;
		std::string			  ProjectId;
		{
			{
				RwLock::SharedLockScope Lock(m_ProjectsLock);
				if (!Project->IsExpired(Lock, GcCtx.ProjectStoreExpireTime()))
				{
					ZEN_DEBUG("ProjectStore::CollectGarbage skipped garbage collect of project '{}'. Project no longer expired.",
							  ProjectId);
					continue;
				}
			}
			RwLock::ExclusiveLockScope _(m_ProjectsLock);
			bool					   Success = Project->PrepareForDelete(PathToRemove);
			if (!Success)
			{
				ZEN_DEBUG("ProjectStore::CollectGarbage skipped garbage collect of project '{}'. Project folder is locked.", ProjectId);
				continue;
			}
			m_Projects.erase(Project->Identifier);
			ProjectId = Project->Identifier;
		}

		ZEN_DEBUG("ProjectStore::CollectGarbage garbage collected project '{}'. Removing storage on disk", ProjectId);
		if (PathToRemove.empty())
		{
			continue;
		}

		DeleteDirectories(PathToRemove);
	}
}

GcStorageSize
ProjectStore::StorageSize() const
{
	ZEN_TRACE_CPU("Store::StorageSize");

	using namespace std::literals;

	GcStorageSize Result;
	{
		if (std::filesystem::exists(m_ProjectBasePath))
		{
			DirectoryContent ProjectsFolderContent;
			GetDirectoryContent(m_ProjectBasePath, DirectoryContent::IncludeDirsFlag, ProjectsFolderContent);

			for (const std::filesystem::path& ProjectBasePath : ProjectsFolderContent.Directories)
			{
				std::filesystem::path ProjectStateFilePath = ProjectBasePath / "Project.zcb"sv;
				if (std::filesystem::exists(ProjectStateFilePath))
				{
					Result.DiskSize += Project::TotalSize(ProjectBasePath);
					DirectoryContent DirContent;
					GetDirectoryContent(ProjectBasePath, DirectoryContent::IncludeDirsFlag, DirContent);
					for (const std::filesystem::path& OplogBasePath : DirContent.Directories)
					{
						Result.DiskSize += Oplog::TotalSize(OplogBasePath);
					}
				}
			}
		}
	}
	return Result;
}

Ref<ProjectStore::Project>
ProjectStore::OpenProject(std::string_view ProjectId)
{
	ZEN_TRACE_CPU("Store::OpenProject");

	{
		RwLock::SharedLockScope _(m_ProjectsLock);

		auto ProjIt = m_Projects.find(std::string{ProjectId});

		if (ProjIt != m_Projects.end())
		{
			return ProjIt->second;
		}
	}

	RwLock::ExclusiveLockScope _(m_ProjectsLock);

	std::filesystem::path BasePath = BasePathForProject(ProjectId);

	if (Project::Exists(BasePath))
	{
		try
		{
			ZEN_INFO("opening project {} @ {}", ProjectId, BasePath);

			Ref<Project>& Prj =
				m_Projects
					.try_emplace(std::string{ProjectId}, Ref<ProjectStore::Project>(new ProjectStore::Project(this, m_CidStore, BasePath)))
					.first->second;
			Prj->Identifier = ProjectId;
			Prj->Read();
			return Prj;
		}
		catch (const std::exception& e)
		{
			ZEN_WARN("failed to open {} @ {} ({})", ProjectId, BasePath, e.what());
			m_Projects.erase(std::string{ProjectId});
		}
	}

	return {};
}

Ref<ProjectStore::Project>
ProjectStore::NewProject(const std::filesystem::path& BasePath,
						 std::string_view			  ProjectId,
						 const std::filesystem::path& RootDir,
						 const std::filesystem::path& EngineRootDir,
						 const std::filesystem::path& ProjectRootDir,
						 const std::filesystem::path& ProjectFilePath)
{
	ZEN_TRACE_CPU("Store::NewProject");

	RwLock::ExclusiveLockScope _(m_ProjectsLock);

	Ref<Project>& Prj =
		m_Projects.try_emplace(std::string{ProjectId}, Ref<ProjectStore::Project>(new ProjectStore::Project(this, m_CidStore, BasePath)))
			.first->second;
	Prj->Identifier		 = ProjectId;
	Prj->RootDir		 = RootDir;
	Prj->EngineRootDir	 = EngineRootDir;
	Prj->ProjectRootDir	 = ProjectRootDir;
	Prj->ProjectFilePath = ProjectFilePath;
	Prj->Write();

	m_UpdateCaptureLock.WithExclusiveLock([&]() {
		if (m_CapturedProjects)
		{
			m_CapturedProjects->push_back(std::string(ProjectId));
		}
	});

	return Prj;
}

bool
ProjectStore::UpdateProject(std::string_view			 ProjectId,
							const std::filesystem::path& RootDir,
							const std::filesystem::path& EngineRootDir,
							const std::filesystem::path& ProjectRootDir,
							const std::filesystem::path& ProjectFilePath)
{
	ZEN_TRACE_CPU("Store::UpdateProject");

	ZEN_INFO("updating project {}", ProjectId);

	RwLock::ExclusiveLockScope ProjectsLock(m_ProjectsLock);

	auto ProjIt = m_Projects.find(std::string{ProjectId});

	if (ProjIt == m_Projects.end())
	{
		return false;
	}
	Ref<ProjectStore::Project> Prj = ProjIt->second;

	Prj->RootDir		 = RootDir;
	Prj->EngineRootDir	 = EngineRootDir;
	Prj->ProjectRootDir	 = ProjectRootDir;
	Prj->ProjectFilePath = ProjectFilePath;
	Prj->Write();

	return true;
}

bool
ProjectStore::RemoveProject(std::string_view ProjectId, std::filesystem::path& OutDeletePath)
{
	RwLock::ExclusiveLockScope ProjectsLock(m_ProjectsLock);

	auto ProjIt = m_Projects.find(std::string{ProjectId});

	if (ProjIt == m_Projects.end())
	{
		return true;
	}

	bool Success = ProjIt->second->PrepareForDelete(OutDeletePath);

	if (!Success)
	{
		return false;
	}
	m_Projects.erase(ProjIt);
	return true;
}

bool
ProjectStore::DeleteProject(std::string_view ProjectId)
{
	ZEN_TRACE_CPU("Store::DeleteProject");

	ZEN_INFO("deleting project {}", ProjectId);

	std::filesystem::path DeletePath;
	if (!RemoveProject(ProjectId, DeletePath))
	{
		return false;
	}

	if (!DeletePath.empty())
	{
		DeleteDirectories(DeletePath);
	}

	return true;
}

bool
ProjectStore::Exists(std::string_view ProjectId)
{
	return Project::Exists(BasePathForProject(ProjectId));
}

CbArray
ProjectStore::GetProjectsList()
{
	ZEN_TRACE_CPU("Store::GetProjectsList");

	using namespace std::literals;

	DiscoverProjects();

	CbWriter Response;
	Response.BeginArray();

	IterateProjects([&Response](ProjectStore::Project& Prj) {
		Response.BeginObject();
		Response << "Id"sv << Prj.Identifier;
		Response << "RootDir"sv << Prj.RootDir.string();
		Response << "ProjectRootDir"sv << PathToUtf8(Prj.ProjectRootDir);
		Response << "EngineRootDir"sv << PathToUtf8(Prj.EngineRootDir);
		Response << "ProjectFilePath"sv << PathToUtf8(Prj.ProjectFilePath);
		Response.EndObject();
	});
	Response.EndArray();
	return Response.Save().AsArray();
}

std::pair<HttpResponseCode, std::string>
ProjectStore::GetProjectFiles(const std::string_view				 ProjectId,
							  const std::string_view				 OplogId,
							  const std::unordered_set<std::string>& WantedFieldNames,
							  CbObject&								 OutPayload)
{
	ZEN_TRACE_CPU("Store::GetProjectFiles");

	using namespace std::literals;

	Ref<ProjectStore::Project> Project = OpenProject(ProjectId);
	if (!Project)
	{
		return {HttpResponseCode::NotFound, fmt::format("Project files request for unknown project '{}'", ProjectId)};
	}
	Project->TouchProject();

	ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
	if (!FoundLog)
	{
		return {HttpResponseCode::NotFound, fmt::format("Project files for unknown oplog '{}/{}'", ProjectId, OplogId)};
	}
	Project->TouchOplog(OplogId);

	const bool WantsAllFields = WantedFieldNames.empty();

	const bool WantsIdField			= WantsAllFields || WantedFieldNames.contains("id");
	const bool WantsClientPathField = WantsAllFields || WantedFieldNames.contains("clientpath");
	const bool WantsServerPathField = WantsAllFields || WantedFieldNames.contains("serverpath");
	const bool WantsRawSizeField	= WantsAllFields || WantedFieldNames.contains("rawsize");
	const bool WantsSizeField		= WantsAllFields || WantedFieldNames.contains("size");

	std::vector<Oid>		 Ids;
	std::vector<std::string> ServerPaths;
	std::vector<std::string> ClientPaths;
	std::vector<uint64_t>	 Sizes;
	std::vector<uint64_t>	 RawSizes;

	size_t Count = 0;
	FoundLog->IterateFileMap([&](const Oid& Id, const std::string_view& ServerPath, const std::string_view& ClientPath) {
		if (WantsIdField || WantsRawSizeField || WantsSizeField)
		{
			Ids.push_back(Id);
		}
		if (WantsServerPathField)
		{
			ServerPaths.push_back(std::string(ServerPath));
		}
		if (WantsClientPathField)
		{
			ClientPaths.push_back(std::string(ClientPath));
		}
		Count++;
	});
	if (WantsRawSizeField || WantsSizeField)
	{
		if (WantsSizeField)
		{
			Sizes.resize(Ids.size(), 0u);
		}
		if (WantsRawSizeField)
		{
			RawSizes.resize(Ids.size(), 0u);
		}

		FoundLog->IterateChunks(
			Ids,
			[&](size_t Index, const IoBuffer& Payload) {
				try
				{
					uint64_t Size = Payload.GetSize();
					if (WantsRawSizeField)
					{
						uint64_t RawSize = Size;
						if (Payload.GetContentType() == ZenContentType::kCompressedBinary)
						{
							IoHash __;
							(void)CompressedBuffer::FromCompressed(SharedBuffer(Payload), __, RawSize);
						}
						RawSizes[Index] = RawSize;
					}
					if (WantsSizeField)
					{
						Sizes[Index] = Size;
					}
				}
				catch (const std::exception& Ex)
				{
					ZEN_WARN("Failed getting project file info for id {}. Reason: '{}'", Ids[Index], Ex.what());
				}
				return true;
			},
			&GetSmallWorkerPool());
	}

	CbObjectWriter Response;
	Response.BeginArray("files"sv);
	for (size_t Index = 0; Index < Count; Index++)
	{
		Response.BeginObject();
		if (WantsIdField)
		{
			Response << "id"sv << Ids[Index];
		}
		if (WantsServerPathField)
		{
			Response << "serverpath"sv << ServerPaths[Index];
		}
		if (WantsClientPathField)
		{
			Response << "clientpath"sv << ClientPaths[Index];
		}
		if (WantsSizeField)
		{
			Response << "size"sv << Sizes[Index];
		}
		if (WantsRawSizeField)
		{
			Response << "rawsize"sv << RawSizes[Index];
		}
		Response.EndObject();
	}
	Response.EndArray();

	OutPayload = Response.Save();
	return {HttpResponseCode::OK, {}};
}

std::pair<HttpResponseCode, std::string>
ProjectStore::GetProjectChunkInfos(const std::string_view				  ProjectId,
								   const std::string_view				  OplogId,
								   const std::unordered_set<std::string>& WantedFieldNames,
								   CbObject&							  OutPayload)
{
	ZEN_TRACE_CPU("ProjectStore::GetProjectChunkInfos");

	using namespace std::literals;

	Ref<ProjectStore::Project> Project = OpenProject(ProjectId);
	if (!Project)
	{
		return {HttpResponseCode::NotFound, fmt::format("unknown project '{}'", ProjectId)};
	}
	Project->TouchProject();

	ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
	if (!FoundLog)
	{
		return {HttpResponseCode::NotFound, fmt::format("unknown oplog '{}/{}'", ProjectId, OplogId)};
	}
	Project->TouchOplog(OplogId);

	const bool WantsAllFields = WantedFieldNames.empty();

	const bool WantsIdField		 = WantsAllFields || WantedFieldNames.contains("id");
	const bool WantsRawHashField = WantsAllFields || WantedFieldNames.contains("rawhash");
	const bool WantsRawSizeField = WantsAllFields || WantedFieldNames.contains("rawsize");
	const bool WantsSizeField	 = WantsAllFields || WantedFieldNames.contains("size");

	std::vector<Oid>	  Ids;
	std::vector<IoHash>	  Hashes;
	std::vector<uint64_t> RawSizes;
	std::vector<uint64_t> Sizes;

	size_t Count = 0;
	FoundLog->IterateChunkMap([&](const Oid& Id, const IoHash& Hash) {
		if (WantsIdField)
		{
			Ids.push_back(Id);
		}
		if (WantsRawHashField || WantsRawSizeField || WantsSizeField)
		{
			Hashes.push_back(Hash);
		}
		Count++;
	});

	if (WantsRawSizeField || WantsSizeField)
	{
		if (WantsRawSizeField)
		{
			RawSizes.resize(Hashes.size(), 0u);
		}
		if (WantsSizeField)
		{
			Sizes.resize(Hashes.size(), 0u);
		}

		WorkerThreadPool& WorkerPool = GetSmallWorkerPool();  // GetSyncWorkerPool();
		(void)FoundLog->IterateChunks(
			Hashes,
			[&](size_t Index, const IoBuffer& Chunk) -> bool {
				try
				{
					uint64_t Size = Chunk.GetSize();
					if (WantsRawSizeField)
					{
						uint64_t RawSize = Size;
						if (Chunk.GetContentType() == ZenContentType::kCompressedBinary)
						{
							IoHash __;
							(void)CompressedBuffer::FromCompressed(SharedBuffer(Chunk), __, RawSize);
						}
						RawSizes[Index] = RawSize;
					}
					if (WantsSizeField)
					{
						Sizes[Index] = Size;
					}
				}
				catch (const std::exception& Ex)
				{
					ZEN_WARN("Failed getting chunk info for id {}. Reason: '{}'", Ids[Index], Ex.what());
				}
				return true;
			},
			&WorkerPool);
	}

	CbObjectWriter Response;
	Response.BeginArray("chunkinfos"sv);

	for (size_t Index = 0; Index < Count; Index++)
	{
		Response.BeginObject();
		if (WantsIdField)
		{
			Response << "id"sv << Ids[Index];
		}
		if (WantsRawHashField)
		{
			Response << "rawhash"sv << Hashes[Index];
		}
		if (WantsSizeField)
		{
			Response << "size"sv << Sizes[Index];
		}
		if (WantsRawSizeField)
		{
			Response << "rawsize"sv << RawSizes[Index];
		}
		Response.EndObject();
	}
	Response.EndArray();

	OutPayload = Response.Save();
	return {HttpResponseCode::OK, {}};
}

std::pair<HttpResponseCode, std::string>
ProjectStore::GetChunkInfo(const std::string_view ProjectId,
						   const std::string_view OplogId,
						   const std::string_view ChunkId,
						   CbObject&			  OutPayload)
{
	using namespace std::literals;

	Ref<ProjectStore::Project> Project = OpenProject(ProjectId);
	if (!Project)
	{
		return {HttpResponseCode::NotFound, fmt::format("Chunk info request for unknown project '{}'", ProjectId)};
	}
	Project->TouchProject();

	ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
	if (!FoundLog)
	{
		return {HttpResponseCode::NotFound, fmt::format("Chunk info request for unknown oplog '{}/{}'", ProjectId, OplogId)};
	}
	Project->TouchOplog(OplogId);

	if (ChunkId.size() != 2 * sizeof(Oid::OidBits))
	{
		return {HttpResponseCode::BadRequest,
				fmt::format("Chunk info request for invalid chunk id '{}/{}'/'{}'", ProjectId, OplogId, ChunkId)};
	}

	const Oid Obj = Oid::FromHexString(ChunkId);

	IoBuffer Chunk = FoundLog->FindChunk(Obj);
	if (!Chunk)
	{
		return {HttpResponseCode::NotFound, {}};
	}

	uint64_t ChunkSize = Chunk.GetSize();
	if (Chunk.GetContentType() == HttpContentType::kCompressedBinary)
	{
		IoHash	 RawHash;
		uint64_t RawSize;
		bool	 IsCompressed = CompressedBuffer::ValidateCompressedHeader(Chunk, RawHash, RawSize);
		if (!IsCompressed)
		{
			return {HttpResponseCode::InternalServerError,
					fmt::format("Chunk info request for malformed chunk id '{}/{}'/'{}'", ProjectId, OplogId, ChunkId)};
		}
		ChunkSize = RawSize;
	}

	CbObjectWriter Response;
	Response << "size"sv << ChunkSize;
	OutPayload = Response.Save();
	return {HttpResponseCode::OK, {}};
}

std::pair<HttpResponseCode, std::string>
ProjectStore::GetChunkRange(const std::string_view ProjectId,
							const std::string_view OplogId,
							const std::string_view ChunkId,
							uint64_t			   Offset,
							uint64_t			   Size,
							ZenContentType		   AcceptType,
							CompositeBuffer&	   OutChunk,
							ZenContentType&		   OutContentType)
{
	if (ChunkId.size() != 2 * sizeof(Oid::OidBits))
	{
		return {HttpResponseCode::BadRequest, fmt::format("Chunk request for invalid chunk id '{}/{}'/'{}'", ProjectId, OplogId, ChunkId)};
	}

	const Oid Obj = Oid::FromHexString(ChunkId);

	return GetChunkRange(ProjectId, OplogId, Obj, Offset, Size, AcceptType, OutChunk, OutContentType);
}

std::pair<HttpResponseCode, std::string>
ProjectStore::GetChunkRange(const std::string_view ProjectId,
							const std::string_view OplogId,
							Oid					   ChunkId,
							uint64_t			   Offset,
							uint64_t			   Size,
							ZenContentType		   AcceptType,
							CompositeBuffer&	   OutChunk,
							ZenContentType&		   OutContentType)
{
	bool IsOffset = Offset != 0 || Size != ~(0ull);

	Ref<ProjectStore::Project> Project = OpenProject(ProjectId);
	if (!Project)
	{
		return {HttpResponseCode::NotFound, fmt::format("Chunk request for unknown project '{}'", ProjectId)};
	}
	Project->TouchProject();

	ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
	if (!FoundLog)
	{
		return {HttpResponseCode::NotFound, fmt::format("Chunk request for unknown oplog '{}/{}'", ProjectId, OplogId)};
	}
	Project->TouchOplog(OplogId);

	IoBuffer Chunk = FoundLog->FindChunk(ChunkId);
	if (!Chunk)
	{
		return {HttpResponseCode::NotFound, {}};
	}

	OutContentType = Chunk.GetContentType();

	if (OutContentType == ZenContentType::kCompressedBinary)
	{
		IoHash			 RawHash;
		uint64_t		 RawSize;
		CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(std::move(Chunk)), RawHash, RawSize);
		ZEN_ASSERT(!Compressed.IsNull());

		if (IsOffset)
		{
			if (Size == ~(0ull) || (Offset + Size) > RawSize)
			{
				Size = RawSize - Offset;
			}

			if (AcceptType == ZenContentType::kBinary)
			{
				OutChunk	   = CompositeBuffer(Compressed.Decompress(Offset, Size));
				OutContentType = ZenContentType::kBinary;
			}
			else
			{
				// Value will be a range of compressed blocks that covers the requested range
				// The client will have to compensate for any offsets that do not land on an even block size multiple
				OutChunk = Compressed.GetRange(Offset, Size).GetCompressed();
			}
		}
		else
		{
			if (AcceptType == ZenContentType::kBinary)
			{
				OutChunk = Compressed.DecompressToComposite();
			}
			else
			{
				OutChunk	   = Compressed.GetCompressed();
				OutContentType = ZenContentType::kCompressedBinary;
			}
		}
	}
	else if (IsOffset)
	{
		if (Size == ~(0ull) || (Offset + Size) > Chunk.GetSize())
		{
			Size = Chunk.GetSize() - Offset;
		}
		OutChunk = CompositeBuffer(SharedBuffer(IoBuffer(std::move(Chunk), Offset, Size)));
	}
	else
	{
		OutChunk = CompositeBuffer(SharedBuffer(std::move(Chunk)));
	}

	return {HttpResponseCode::OK, {}};
}

std::pair<HttpResponseCode, std::string>
ProjectStore::GetChunk(const std::string_view ProjectId,
					   const std::string_view OplogId,
					   const std::string_view Cid,
					   ZenContentType		  AcceptType,
					   IoBuffer&			  OutChunk)
{
	Ref<ProjectStore::Project> Project = OpenProject(ProjectId);
	if (!Project)
	{
		return {HttpResponseCode::NotFound, fmt::format("Chunk request for unknown project '{}'", ProjectId)};
	}
	Project->TouchProject();

	ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
	if (!FoundLog)
	{
		return {HttpResponseCode::NotFound, fmt::format("Chunk request for unknown oplog '{}/{}'", ProjectId, OplogId)};
	}
	Project->TouchOplog(OplogId);

	if (Cid.length() != IoHash::StringLength)
	{
		return {HttpResponseCode::BadRequest, fmt::format("Chunk request for invalid chunk id '{}/{}'/'{}'", ProjectId, OplogId, Cid)};
	}

	const IoHash Hash = IoHash::FromHexString(Cid);
	OutChunk		  = m_CidStore.FindChunkByCid(Hash);

	if (!OutChunk)
	{
		return {HttpResponseCode::NotFound, fmt::format("chunk - '{}' MISSING", Cid)};
	}

	if (AcceptType == ZenContentType::kUnknownContentType || AcceptType == ZenContentType::kBinary)
	{
		CompressedBuffer Compressed = CompressedBuffer::FromCompressedNoValidate(std::move(OutChunk));
		OutChunk					= Compressed.Decompress().AsIoBuffer();
		OutChunk.SetContentType(ZenContentType::kBinary);
	}
	else
	{
		OutChunk.SetContentType(ZenContentType::kCompressedBinary);
	}
	return {HttpResponseCode::OK, {}};
}

std::pair<HttpResponseCode, std::string>
ProjectStore::PutChunk(const std::string_view ProjectId,
					   const std::string_view OplogId,
					   const std::string_view Cid,
					   ZenContentType		  ContentType,
					   IoBuffer&&			  Chunk)
{
	Ref<ProjectStore::Project> Project = OpenProject(ProjectId);
	if (!Project)
	{
		return {HttpResponseCode::NotFound, fmt::format("Chunk put request for unknown project '{}'", ProjectId)};
	}
	Project->TouchProject();

	ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
	if (!FoundLog)
	{
		return {HttpResponseCode::NotFound, fmt::format("Chunk put request for unknown oplog '{}/{}'", ProjectId, OplogId)};
	}
	Project->TouchOplog(OplogId);

	if (Cid.length() != IoHash::StringLength)
	{
		return {HttpResponseCode::BadRequest, fmt::format("Chunk put request for invalid chunk hash '{}'", Cid)};
	}

	const IoHash Hash = IoHash::FromHexString(Cid);

	if (ContentType != HttpContentType::kCompressedBinary)
	{
		return {HttpResponseCode::BadRequest, fmt::format("Chunk request for invalid content type for chunk '{}'", Cid)};
	}
	IoHash			 RawHash;
	uint64_t		 RawSize;
	CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Chunk), RawHash, RawSize);
	if (RawHash != Hash)
	{
		return {HttpResponseCode::BadRequest, fmt::format("Chunk request for invalid payload format for chunk '{}'", Cid)};
	}

	CidStore::InsertResult Result = m_CidStore.AddChunk(Chunk, Hash);
	return {Result.New ? HttpResponseCode::Created : HttpResponseCode::OK, {}};
}

std::pair<HttpResponseCode, std::string>
ProjectStore::WriteOplog(const std::string_view ProjectId, const std::string_view OplogId, IoBuffer&& Payload, CbObject& OutResponse)
{
	ZEN_TRACE_CPU("Store::WriteOplog");

	Ref<ProjectStore::Project> Project = OpenProject(ProjectId);
	if (!Project)
	{
		return {HttpResponseCode::NotFound, fmt::format("Write oplog request for unknown project '{}'", ProjectId)};
	}
	Project->TouchProject();

	ProjectStore::Oplog* Oplog = Project->OpenOplog(OplogId);
	if (!Oplog)
	{
		return {HttpResponseCode::NotFound, fmt::format("Write oplog request for unknown oplog '{}/{}'", ProjectId, OplogId)};
	}
	Project->TouchOplog(OplogId);

	CbObject ContainerObject = LoadCompactBinaryObject(Payload);
	if (!ContainerObject)
	{
		return {HttpResponseCode::BadRequest, "Invalid payload format"};
	}

	CidStore&							   ChunkStore = m_CidStore;
	RwLock								   AttachmentsLock;
	tsl::robin_set<IoHash, IoHash::Hasher> Attachments;

	auto HasAttachment = [&ChunkStore](const IoHash& RawHash) { return ChunkStore.ContainsChunk(RawHash); };
	auto OnNeedBlock   = [&AttachmentsLock, &Attachments](const IoHash& BlockHash, const std::vector<IoHash>&& ChunkHashes) {
		  RwLock::ExclusiveLockScope _(AttachmentsLock);
		  if (BlockHash != IoHash::Zero)
		  {
			  Attachments.insert(BlockHash);
		  }
		  else
		  {
			  Attachments.insert(ChunkHashes.begin(), ChunkHashes.end());
		  }
	};
	auto OnNeedAttachment = [&AttachmentsLock, &Attachments](const IoHash& RawHash) {
		RwLock::ExclusiveLockScope _(AttachmentsLock);
		Attachments.insert(RawHash);
	};

	auto OnChunkedAttachment = [](const ChunkedInfo&) {};

	auto OnReferencedAttachments = [&Oplog](std::span<IoHash> RawHashes) { Oplog->CaptureAddedAttachments(RawHashes); };
	// Make sure we retain any attachments we download before writing the oplog
	Oplog->EnableUpdateCapture();
	auto _ = MakeGuard([&Oplog]() { Oplog->DisableUpdateCapture(); });

	RemoteProjectStore::Result RemoteResult = SaveOplogContainer(*Oplog,
																 ContainerObject,
																 OnReferencedAttachments,
																 HasAttachment,
																 OnNeedBlock,
																 OnNeedAttachment,
																 OnChunkedAttachment,
																 nullptr);

	if (RemoteResult.ErrorCode)
	{
		return ConvertResult(RemoteResult);
	}

	CbObjectWriter Cbo;
	Cbo.BeginArray("need");
	{
		for (const IoHash& Hash : Attachments)
		{
			ZEN_DEBUG("Need attachment {}", Hash);
			Cbo << Hash;
		}
	}
	Cbo.EndArray();	 // "need"

	OutResponse = Cbo.Save();
	return {HttpResponseCode::OK, {}};
}

std::pair<HttpResponseCode, std::string>
ProjectStore::ReadOplog(const std::string_view				  ProjectId,
						const std::string_view				  OplogId,
						const HttpServerRequest::QueryParams& Params,
						CbObject&							  OutResponse)
{
	ZEN_TRACE_CPU("Store::ReadOplog");

	Ref<ProjectStore::Project> Project = OpenProject(ProjectId);
	if (!Project)
	{
		return {HttpResponseCode::NotFound, fmt::format("Read oplog request for unknown project '{}'", ProjectId)};
	}
	Project->TouchProject();

	ProjectStore::Oplog* Oplog = Project->OpenOplog(OplogId);
	if (!Oplog)
	{
		return {HttpResponseCode::NotFound, fmt::format("Read oplog request for unknown oplog '{}/{}'", ProjectId, OplogId)};
	}
	Project->TouchOplog(OplogId);

	size_t MaxBlockSize = RemoteStoreOptions::DefaultMaxBlockSize;
	if (auto Param = Params.GetValue("maxblocksize"); Param.empty() == false)
	{
		if (auto Value = ParseInt<size_t>(Param))
		{
			MaxBlockSize = Value.value();
		}
	}
	size_t MaxChunkEmbedSize = RemoteStoreOptions::DefaultMaxChunkEmbedSize;
	if (auto Param = Params.GetValue("maxchunkembedsize"); Param.empty() == false)
	{
		if (auto Value = ParseInt<size_t>(Param))
		{
			MaxChunkEmbedSize = Value.value();
		}
	}

	size_t ChunkFileSizeLimit = RemoteStoreOptions::DefaultChunkFileSizeLimit;
	if (auto Param = Params.GetValue("chunkfilesizelimit"); Param.empty() == false)
	{
		if (auto Value = ParseInt<size_t>(Param))
		{
			ChunkFileSizeLimit = Value.value();
		}
	}

	CidStore& ChunkStore = m_CidStore;

	RemoteProjectStore::LoadContainerResult ContainerResult = BuildContainer(
		ChunkStore,
		*Project.Get(),
		*Oplog,
		MaxBlockSize,
		MaxChunkEmbedSize,
		ChunkFileSizeLimit,
		/* BuildBlocks */ false,
		/* IgnoreMissingAttachments */ false,
		/* AllowChunking*/ false,
		[](CompressedBuffer&&, const IoHash&) {},
		[](const IoHash&, TGetAttachmentBufferFunc&&) {},
		[](std::vector<std::pair<IoHash, FetchChunkFunc>>&&) {},
		/* EmbedLooseFiles*/ false);

	OutResponse = std::move(ContainerResult.ContainerObject);
	return ConvertResult(ContainerResult);
}

bool
ProjectStore::Rpc(HttpServerRequest&	 HttpReq,
				  const std::string_view ProjectId,
				  const std::string_view OplogId,
				  IoBuffer&&			 Payload,
				  AuthMgr&				 AuthManager)
{
	ZEN_TRACE_CPU("Store::Rpc");

	using namespace std::literals;
	HttpContentType PayloadContentType = HttpReq.RequestContentType();
	CbPackage		Package;
	CbObject		Cb;
	switch (PayloadContentType)
	{
		case HttpContentType::kJSON:
		case HttpContentType::kUnknownContentType:
		case HttpContentType::kText:
			{
				std::string JsonText(reinterpret_cast<const char*>(Payload.GetData()), Payload.GetSize());
				Cb = LoadCompactBinaryFromJson(JsonText).AsObject();
				if (!Cb)
				{
					HttpReq.WriteResponse(HttpResponseCode::BadRequest,
										  HttpContentType::kText,
										  "Content format not supported, expected JSON format");
					return false;
				}
			}
			break;
		case HttpContentType::kCbObject:
			Cb = LoadCompactBinaryObject(Payload);
			if (!Cb)
			{
				HttpReq.WriteResponse(HttpResponseCode::BadRequest,
									  HttpContentType::kText,
									  "Content format not supported, expected compact binary format");
				return false;
			}
			break;
		case HttpContentType::kCbPackage:
			try
			{
				Package = ParsePackageMessage(Payload);
				Cb		= Package.GetObject();
			}
			catch (const std::invalid_argument& ex)
			{
				HttpReq.WriteResponse(HttpResponseCode::BadRequest,
									  HttpContentType::kText,
									  fmt::format("Failed to parse package request, reason: '{}'", ex.what()));
				return false;
			}
			if (!Cb)
			{
				HttpReq.WriteResponse(HttpResponseCode::BadRequest,
									  HttpContentType::kText,
									  "Content format not supported, expected package message format");
				return false;
			}
			break;
		default:
			HttpReq.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Invalid request content type");
			return false;
	}

	Ref<ProjectStore::Project> Project = OpenProject(ProjectId);
	if (!Project)
	{
		HttpReq.WriteResponse(HttpResponseCode::NotFound,
							  HttpContentType::kText,
							  fmt::format("Rpc oplog request for unknown project '{}'", ProjectId));
		return true;
	}
	Project->TouchProject();

	ProjectStore::Oplog* Oplog = Project->OpenOplog(OplogId);
	if (!Oplog)
	{
		HttpReq.WriteResponse(HttpResponseCode::NotFound,
							  HttpContentType::kText,
							  fmt::format("Rpc oplog request for unknown oplog '{}/{}'", ProjectId, OplogId));
		return true;
	}
	Project->TouchOplog(OplogId);

	std::string_view Method = Cb["method"sv].AsString();

	if (Method == "import"sv)
	{
		if (!AreDiskWritesAllowed())
		{
			HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage);
			return true;
		}
		std::pair<HttpResponseCode, std::string> Result = Import(*Project.Get(), *Oplog, Cb["params"sv].AsObjectView(), AuthManager);
		if (Result.second.empty())
		{
			HttpReq.WriteResponse(Result.first);
			return Result.first != HttpResponseCode::BadRequest;
		}
		HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second);
		return true;
	}
	else if (Method == "export"sv)
	{
		std::pair<HttpResponseCode, std::string> Result = Export(Project, *Oplog, Cb["params"sv].AsObjectView(), AuthManager);
		HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second);
		return true;
	}
	else if (Method == "getchunks"sv)
	{
		ZEN_TRACE_CPU("Store::Rpc::getchunks");
		CbPackage ResponsePackage;
		{
			CbArrayView	   ChunksArray = Cb["chunks"sv].AsArrayView();
			CbObjectWriter ResponseWriter;
			ResponseWriter.BeginArray("chunks"sv);
			for (CbFieldView FieldView : ChunksArray)
			{
				IoHash	 RawHash	 = FieldView.AsHash();
				IoBuffer ChunkBuffer = m_CidStore.FindChunkByCid(RawHash);
				if (ChunkBuffer)
				{
					CompressedBuffer Compressed = CompressedBuffer::FromCompressedNoValidate(std::move(ChunkBuffer));
					if (Compressed)
					{
						ResponseWriter.AddHash(RawHash);
						ResponsePackage.AddAttachment(CbAttachment(std::move(Compressed), RawHash));
					}
					else
					{
						ZEN_WARN("invalid compressed binary in cas store for {}", RawHash);
					}
				}
			}
			ResponseWriter.EndArray();
			ResponsePackage.SetObject(ResponseWriter.Save());
		}
		CompositeBuffer RpcResponseBuffer = FormatPackageMessageBuffer(ResponsePackage, FormatFlags::kDefault);
		HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kCbPackage, RpcResponseBuffer);
		return true;
	}
	else if (Method == "putchunks"sv)
	{
		ZEN_TRACE_CPU("Store::Rpc::putchunks");
		if (!AreDiskWritesAllowed())
		{
			HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage);
			return true;
		}

		std::span<const CbAttachment> Attachments = Package.GetAttachments();
		if (!Attachments.empty())
		{
			std::vector<IoBuffer> WriteAttachmentBuffers;
			std::vector<IoHash>	  WriteRawHashes;

			WriteAttachmentBuffers.reserve(Attachments.size());
			WriteRawHashes.reserve(Attachments.size());

			for (const CbAttachment& Attachment : Attachments)
			{
				IoHash					RawHash	   = Attachment.GetHash();
				const CompressedBuffer& Compressed = Attachment.AsCompressedBinary();
				WriteAttachmentBuffers.push_back(Compressed.GetCompressed().Flatten().AsIoBuffer());
				WriteRawHashes.push_back(RawHash);
			}

			m_CidStore.AddChunks(WriteAttachmentBuffers, WriteRawHashes, CidStore::InsertMode::kCopyOnly);
		}
		HttpReq.WriteResponse(HttpResponseCode::OK);
		return true;
	}
	else if (Method == "snapshot"sv)
	{
		ZEN_TRACE_CPU("Store::Rpc::snapshot");
		if (!AreDiskWritesAllowed())
		{
			HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage);
			return true;
		}

		// Snapshot all referenced files. This brings the content of all
		// files into the CID store

		int		 OpCount	  = 0;
		uint64_t InlinedBytes = 0;
		uint64_t InlinedFiles = 0;
		uint64_t TotalBytes	  = 0;
		uint64_t TotalFiles	  = 0;

		std::vector<CbObject>						 NewOps;
		std::unordered_map<Oid, IoHash, Oid::Hasher> NewChunkMappings;

		Oplog->IterateOplog([&](CbObjectView Op) {
			bool OpRewritten = false;
			bool AllOk		 = true;

			CbWriter Cbo;
			Cbo.BeginArray("files"sv);

			for (CbFieldView& Field : Op["files"sv])
			{
				bool CopyField = true;

				if (CbObjectView View = Field.AsObjectView())
				{
					const IoHash DataHash = View["data"sv].AsHash();

					if (DataHash == IoHash::Zero)
					{
						std::string_view	  ServerPath = View["serverpath"sv].AsString();
						std::filesystem::path FilePath	 = Project->RootDir / ServerPath;
						BasicFile			  DataFile;
						std::error_code		  Ec;
						DataFile.Open(FilePath, BasicFile::Mode::kRead, Ec);

						if (Ec)
						{
							// Error...

							ZEN_ERROR("unable to read data from file '{}': {}", FilePath, Ec.message());

							AllOk = false;
						}
						else
						{
							// Read file contents into memory, compress and store in CidStore

							Oid					   ChunkId			= View["id"sv].AsObjectId();
							IoBuffer			   FileIoBuffer		= DataFile.ReadAll();
							CompressedBuffer	   Compressed		= CompressedBuffer::Compress(SharedBuffer(std::move(FileIoBuffer)));
							const IoHash		   RawHash			= Compressed.DecodeRawHash();
							const uint64_t		   RawSize			= Compressed.DecodeRawSize();
							IoBuffer			   CompressedBuffer = Compressed.GetCompressed().Flatten().AsIoBuffer();
							CidStore::InsertResult Result			= m_CidStore.AddChunk(CompressedBuffer, RawHash);

							TotalBytes += RawSize;
							++TotalFiles;

							if (Result.New)
							{
								InlinedBytes += RawSize;
								++InlinedFiles;
							}

							// Rewrite file array entry with new data reference
							CbObjectWriter Writer;
							RewriteCbObject(Writer, View, [&](CbObjectWriter&, CbFieldView Field) -> bool {
								if (Field.GetName() == "data"sv)
								{
									// omit this field as we will write it explicitly ourselves
									return true;
								}
								return false;
							});
							Writer.AddBinaryAttachment("data"sv, RawHash);

							CbObject RewrittenOp = Writer.Save();
							Cbo.AddObject(std::move(RewrittenOp));
							CopyField = false;

							NewChunkMappings.insert_or_assign(ChunkId, RawHash);
						}
					}
				}

				if (CopyField)
				{
					Cbo.AddField(Field);
				}
				else
				{
					OpRewritten = true;
				}
			}

			if (OpRewritten && AllOk)
			{
				Cbo.EndArray();
				CbArray FilesArray = Cbo.Save().AsArray();

				CbObject RewrittenOp = RewriteCbObject(Op, [&](CbObjectWriter& NewWriter, CbFieldView Field) -> bool {
					if (Field.GetName() == "files"sv)
					{
						NewWriter.AddArray("files"sv, FilesArray);

						return true;
					}

					return false;
				});

				NewOps.push_back(std::move(RewrittenOp));
			}

			OpCount++;
		});

		// Make sure we have references to our attachments
		Oplog->AddChunkMappings(NewChunkMappings);

		CbObjectWriter ResponseObj;

		// Persist rewritten oplog entries

		if (!NewOps.empty())
		{
			ResponseObj.BeginArray("rewritten_ops");

			for (CbObject& NewOp : NewOps)
			{
				uint32_t NewLsn = Oplog->AppendNewOplogEntry(std::move(NewOp));

				ZEN_DEBUG("appended rewritten op at LSN: {}", NewLsn);

				ResponseObj.AddInteger(NewLsn);
			}

			ResponseObj.EndArray();
		}

		ResponseObj << "inlined_bytes" << InlinedBytes << "inlined_files" << InlinedFiles;
		ResponseObj << "total_bytes" << TotalBytes << "total_files" << TotalFiles;

		ZEN_INFO("rewrote {} oplog entries (out of {})", NewOps.size(), OpCount);

		HttpReq.WriteResponse(HttpResponseCode::OK, ResponseObj.Save());
		return true;
	}
	HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, fmt::format("Unknown rpc method '{}'", Method));
	return true;
}

std::pair<HttpResponseCode, std::string>
ProjectStore::Export(Ref<ProjectStore::Project> Project, ProjectStore::Oplog& Oplog, CbObjectView&& Params, AuthMgr& AuthManager)
{
	ZEN_TRACE_CPU("Store::Export");

	using namespace std::literals;

	size_t MaxBlockSize				= Params["maxblocksize"sv].AsUInt64(RemoteStoreOptions::DefaultMaxBlockSize);
	size_t MaxChunkEmbedSize		= Params["maxchunkembedsize"sv].AsUInt64(RemoteStoreOptions::DefaultMaxChunkEmbedSize);
	size_t ChunkFileSizeLimit		= Params["chunkfilesizelimit"sv].AsUInt64(RemoteStoreOptions::DefaultChunkFileSizeLimit);
	bool   Force					= Params["force"sv].AsBool(false);
	bool   IgnoreMissingAttachments = Params["ignoremissingattachments"sv].AsBool(false);
	bool   EmbedLooseFile			= Params["embedloosefiles"sv].AsBool(false);

	CreateRemoteStoreResult RemoteStoreResult = CreateRemoteStore(Params, AuthManager, MaxBlockSize, MaxChunkEmbedSize, Oplog.TempPath());

	if (RemoteStoreResult.Store == nullptr)
	{
		return {HttpResponseCode::BadRequest, RemoteStoreResult.Description};
	}
	std::shared_ptr<RemoteProjectStore> RemoteStore = std::move(RemoteStoreResult.Store);
	RemoteProjectStore::RemoteStoreInfo StoreInfo	= RemoteStore->GetInfo();

	ZEN_INFO("Saving oplog '{}/{}' to {}, maxblocksize {}, maxchunkembedsize {}",
			 Project->Identifier,
			 Oplog.OplogId(),
			 StoreInfo.Description,
			 NiceBytes(MaxBlockSize),
			 NiceBytes(MaxChunkEmbedSize));

	JobId JobId = m_JobQueue.QueueJob(
		fmt::format("Export oplog '{}/{}' to {}", Project->Identifier, Oplog.OplogId(), StoreInfo.Description),
		[this,
		 ActualRemoteStore = std::move(RemoteStore),
		 Project,
		 OplogPtr = &Oplog,
		 MaxBlockSize,
		 MaxChunkEmbedSize,
		 ChunkFileSizeLimit,
		 EmbedLooseFile,
		 Force,
		 IgnoreMissingAttachments](JobContext& Context) {
			RemoteProjectStore::Result Result	= SaveOplog(m_CidStore,
															*ActualRemoteStore,
															*Project.Get(),
															*OplogPtr,
															MaxBlockSize,
															MaxChunkEmbedSize,
															ChunkFileSizeLimit,
															EmbedLooseFile,
															Force,
															IgnoreMissingAttachments,
															&Context);
			auto					   Response = ConvertResult(Result);
			ZEN_INFO("SaveOplog: Status: {} '{}'", ToString(Response.first), Response.second);
			if (!IsHttpSuccessCode(Response.first))
			{
				throw std::runtime_error(Response.second.empty() ? fmt::format("Status: {}", ToString(Response.first)) : Response.second);
			}
		});

	return {HttpResponseCode::Accepted, fmt::format("{}", JobId.Id)};
}

std::pair<HttpResponseCode, std::string>
ProjectStore::Import(ProjectStore::Project& Project, ProjectStore::Oplog& Oplog, CbObjectView&& Params, AuthMgr& AuthManager)
{
	ZEN_TRACE_CPU("Store::Import");

	using namespace std::literals;

	size_t MaxBlockSize				= Params["maxblocksize"sv].AsUInt64(RemoteStoreOptions::DefaultMaxBlockSize);
	size_t MaxChunkEmbedSize		= Params["maxchunkembedsize"sv].AsUInt64(RemoteStoreOptions::DefaultMaxChunkEmbedSize);
	bool   Force					= Params["force"sv].AsBool(false);
	bool   IgnoreMissingAttachments = Params["ignoremissingattachments"sv].AsBool(false);
	bool   CleanOplog				= Params["clean"].AsBool(false);

	CreateRemoteStoreResult RemoteStoreResult = CreateRemoteStore(Params, AuthManager, MaxBlockSize, MaxChunkEmbedSize, Oplog.TempPath());

	if (RemoteStoreResult.Store == nullptr)
	{
		return {HttpResponseCode::BadRequest, RemoteStoreResult.Description};
	}
	std::shared_ptr<RemoteProjectStore> RemoteStore = std::move(RemoteStoreResult.Store);
	RemoteProjectStore::RemoteStoreInfo StoreInfo	= RemoteStore->GetInfo();

	ZEN_INFO("Loading oplog '{}/{}' from {}", Project.Identifier, Oplog.OplogId(), StoreInfo.Description);
	JobId JobId = m_JobQueue.QueueJob(
		fmt::format("Import oplog '{}/{}' from {}", Project.Identifier, Oplog.OplogId(), StoreInfo.Description),
		[this, ActualRemoteStore = std::move(RemoteStore), OplogPtr = &Oplog, Force, IgnoreMissingAttachments, CleanOplog](
			JobContext& Context) {
			RemoteProjectStore::Result Result =
				LoadOplog(m_CidStore, *ActualRemoteStore, *OplogPtr, Force, IgnoreMissingAttachments, CleanOplog, &Context);
			auto Response = ConvertResult(Result);
			ZEN_INFO("LoadOplog: Status: {} '{}'", ToString(Response.first), Response.second);
			if (!IsHttpSuccessCode(Response.first))
			{
				throw std::runtime_error(Response.second.empty() ? fmt::format("Status: {}", ToString(Response.first)) : Response.second);
			}
		});

	return {HttpResponseCode::Accepted, fmt::format("{}", JobId.Id)};
}

bool
ProjectStore::AreDiskWritesAllowed() const
{
	return (m_DiskWriteBlocker == nullptr || m_DiskWriteBlocker->AreDiskWritesAllowed());
}

void
ProjectStore::EnableUpdateCapture()
{
	m_UpdateCaptureLock.WithExclusiveLock([&]() {
		if (m_UpdateCaptureRefCounter == 0)
		{
			ZEN_ASSERT(!m_CapturedProjects);
			m_CapturedProjects = std::make_unique<std::vector<std::string>>();
		}
		else
		{
			ZEN_ASSERT(m_CapturedProjects);
		}
		m_UpdateCaptureRefCounter++;
	});
}

void
ProjectStore::DisableUpdateCapture()
{
	m_UpdateCaptureLock.WithExclusiveLock([&]() {
		ZEN_ASSERT(m_CapturedProjects);
		ZEN_ASSERT(m_UpdateCaptureRefCounter > 0);
		m_UpdateCaptureRefCounter--;
		if (m_UpdateCaptureRefCounter == 0)
		{
			m_CapturedProjects.reset();
		}
	});
}

std::vector<std::string>
ProjectStore::GetCapturedProjects()
{
	RwLock::SharedLockScope _(m_UpdateCaptureLock);
	if (m_CapturedProjects)
	{
		return *m_CapturedProjects;
	}
	return {};
}

std::string
ProjectStore::GetGcName(GcCtx&)
{
	return fmt::format("projectstore: '{}'", m_ProjectBasePath.string());
}

class ProjectStoreGcStoreCompactor : public GcStoreCompactor
{
public:
	ProjectStoreGcStoreCompactor(const std::filesystem::path&		  BasePath,
								 std::vector<std::filesystem::path>&& OplogPathsToRemove,
								 std::vector<std::filesystem::path>&& ProjectPathsToRemove)
	: m_BasePath(BasePath)
	, m_OplogPathsToRemove(std::move(OplogPathsToRemove))
	, m_ProjectPathsToRemove(std::move(ProjectPathsToRemove))
	{
	}

	virtual void CompactStore(GcCtx& Ctx, GcCompactStoreStats& Stats, const std::function<uint64_t()>&) override
	{
		ZEN_TRACE_CPU("Store::CompactStore");

		Stopwatch  Timer;
		const auto _ = MakeGuard([&] {
			if (!Ctx.Settings.Verbose)
			{
				return;
			}
			ZEN_INFO("GCV2: projectstore [COMPACT] '{}': RemovedDisk: {} in {}",
					 m_BasePath,
					 NiceBytes(Stats.RemovedDisk),
					 NiceTimeSpanMs(Timer.GetElapsedTimeMs()));
		});

		if (Ctx.Settings.IsDeleteMode)
		{
			for (const std::filesystem::path& OplogPath : m_OplogPathsToRemove)
			{
				uint64_t OplogSize = ProjectStore::Oplog::TotalSize(OplogPath);
				if (DeleteDirectories(OplogPath))
				{
					ZEN_DEBUG("GCV2: projectstore [COMPACT] '{}': removed oplog folder '{}', removed {}",
							  m_BasePath,
							  OplogPath,
							  NiceBytes(OplogSize));
					Stats.RemovedDisk += OplogSize;
				}
				else
				{
					ZEN_WARN("GCV2: projectstore [COMPACT] '{}': Failed to remove oplog folder '{}'", m_BasePath, OplogPath);
				}
			}

			for (const std::filesystem::path& ProjectPath : m_ProjectPathsToRemove)
			{
				uint64_t ProjectSize = ProjectStore::Project::TotalSize(ProjectPath);
				if (DeleteDirectories(ProjectPath))
				{
					ZEN_DEBUG("GCV2: projectstore [COMPACT] '{}': removed project folder '{}', removed {}",
							  m_BasePath,
							  ProjectPath,
							  NiceBytes(ProjectSize));
					Stats.RemovedDisk += ProjectSize;
				}
				else
				{
					ZEN_WARN("GCV2: projectstore [COMPACT] '{}': Failed to remove project folder '{}'", m_BasePath, ProjectPath);
				}
			}
		}
		else
		{
			ZEN_DEBUG("GCV2: projectstore [COMPACT] '{}': Skipped deleting of {} oplogs and {} projects",
					  m_BasePath,
					  m_OplogPathsToRemove.size(),
					  m_ProjectPathsToRemove.size());
		}

		m_ProjectPathsToRemove.clear();
		m_OplogPathsToRemove.clear();
	}

	virtual std::string GetGcName(GcCtx&) override { return fmt::format("projectstore: '{}'", m_BasePath.string()); }

private:
	std::filesystem::path			   m_BasePath;
	std::vector<std::filesystem::path> m_OplogPathsToRemove;
	std::vector<std::filesystem::path> m_ProjectPathsToRemove;
};

GcStoreCompactor*
ProjectStore::RemoveExpiredData(GcCtx& Ctx, GcStats& Stats)
{
	ZEN_TRACE_CPU("Store::RemoveExpiredData");

	Stopwatch  Timer;
	const auto _ = MakeGuard([&] {
		if (!Ctx.Settings.Verbose)
		{
			return;
		}
		ZEN_INFO("GCV2: projectstore [REMOVE EXPIRED] '{}': Count: {}, Expired: {}, Deleted: {} in {}",
				 m_ProjectBasePath,
				 Stats.CheckedCount,
				 Stats.FoundCount,
				 Stats.DeletedCount,
				 NiceTimeSpanMs(Timer.GetElapsedTimeMs()));
	});

	std::vector<std::filesystem::path> OplogPathsToRemove;
	std::vector<std::filesystem::path> ProjectPathsToRemove;

	std::vector<Ref<Project>> ExpiredProjects;
	std::vector<Ref<Project>> Projects;

	DiscoverProjects();

	{
		RwLock::SharedLockScope Lock(m_ProjectsLock);
		for (auto& Kv : m_Projects)
		{
			Stats.CheckedCount++;
			if (Kv.second->IsExpired(Lock, Ctx.Settings.ProjectStoreExpireTime))
			{
				ExpiredProjects.push_back(Kv.second);
				continue;
			}
			Projects.push_back(Kv.second);
		}
	}

	for (const Ref<Project>& Project : Projects)
	{
		std::vector<std::string> OpLogs = Project->ScanForOplogs();
		for (const std::string& OpLogId : OpLogs)
		{
			Project->OpenOplog(OpLogId);
			if (Ctx.IsCancelledFlag)
			{
				return nullptr;
			}
		}
	}

	size_t ExpiredOplogCount = 0;
	for (const Ref<Project>& Project : Projects)
	{
		if (Ctx.IsCancelledFlag)
		{
			break;
		}

		std::vector<std::string> ExpiredOplogs;
		{
			Project->IterateOplogs(
				[&Ctx, &Stats, &Project, &ExpiredOplogs](const RwLock::SharedLockScope& Lock, ProjectStore::Oplog& Oplog) {
					Stats.CheckedCount++;
					if (Project->IsExpired(Lock, Ctx.Settings.ProjectStoreExpireTime, Oplog))
					{
						ExpiredOplogs.push_back(Oplog.OplogId());
					}
				});
		}
		std::filesystem::path ProjectPath = BasePathForProject(Project->Identifier);
		ExpiredOplogCount += ExpiredOplogs.size();
		if (Ctx.Settings.IsDeleteMode)
		{
			for (const std::string& OplogId : ExpiredOplogs)
			{
				std::filesystem::path RemovePath = Project->RemoveOplog(OplogId);
				if (!RemovePath.empty())
				{
					OplogPathsToRemove.push_back(RemovePath);
				}
			}
			Stats.DeletedCount += ExpiredOplogs.size();
			Project->Flush();
		}
	}

	if (ExpiredProjects.empty() && ExpiredOplogCount == 0)
	{
		ZEN_DEBUG("GCV2: projectstore [REMOVE EXPIRED] '{}': no expired projects found", m_ProjectBasePath);
		return nullptr;
	}

	if (Ctx.Settings.IsDeleteMode)
	{
		for (const Ref<Project>& Project : ExpiredProjects)
		{
			std::string ProjectId = Project->Identifier;
			{
				{
					RwLock::SharedLockScope Lock(m_ProjectsLock);
					if (!Project->IsExpired(Lock, Ctx.Settings.ProjectStoreExpireTime))
					{
						ZEN_DEBUG(
							"GCV2: projectstore [REMOVE EXPIRED] '{}': skipped garbage collect of project '{}'. Project no longer "
							"expired.",
							m_ProjectBasePath,
							ProjectId);
						continue;
					}
				}
				std::filesystem::path RemovePath;
				bool				  Success = RemoveProject(ProjectId, RemovePath);
				if (!Success)
				{
					ZEN_DEBUG(
						"GCV2: projectstore [REMOVE EXPIRED] '{}': skipped garbage collect of project '{}'. Project folder is locked.",
						m_ProjectBasePath,
						ProjectId);
					continue;
				}
				if (!RemovePath.empty())
				{
					ProjectPathsToRemove.push_back(RemovePath);
				}
			}
		}
		Stats.DeletedCount += ExpiredProjects.size();
	}

	size_t ExpiredProjectCount = ExpiredProjects.size();
	Stats.FoundCount += ExpiredOplogCount + ExpiredProjectCount;
	if (!OplogPathsToRemove.empty() || !ProjectPathsToRemove.empty())
	{
		return new ProjectStoreGcStoreCompactor(m_ProjectBasePath, std::move(OplogPathsToRemove), std::move(ProjectPathsToRemove));
	}
	return nullptr;
}

class ProjectStoreReferenceChecker : public GcReferenceChecker
{
public:
	ProjectStoreReferenceChecker(ProjectStore& InProjectStore) : m_ProjectStore(InProjectStore) { m_ProjectStore.EnableUpdateCapture(); }

	virtual ~ProjectStoreReferenceChecker()
	{
		try
		{
			m_ProjectStore.DisableUpdateCapture();
		}
		catch (const std::exception& Ex)
		{
			ZEN_ERROR("~ProjectStoreReferenceChecker threw exception: '{}'", Ex.what());
		}
	}

	virtual std::string GetGcName(GcCtx&) override { return "projectstore"; }
	virtual void		PreCache(GcCtx&) override {}

	virtual void UpdateLockedState(GcCtx& Ctx) override
	{
		ZEN_TRACE_CPU("Store::UpdateLockedState");

		Stopwatch Timer;

		std::vector<ProjectStore::Oplog*> AddedOplogs;

		const auto _ = MakeGuard([&] {
			if (!Ctx.Settings.Verbose)
			{
				return;
			}
			ZEN_INFO("GCV2: projectstore [LOCKSTATE] '{}': found {} references in {} in {} new oplogs",
					 "projectstore",
					 m_References.size(),
					 NiceTimeSpanMs(Timer.GetElapsedTimeMs()),
					 AddedOplogs.size());
		});

		std::vector<std::string> AddedProjects = m_ProjectStore.GetCapturedProjects();
		for (const std::string& AddedProject : AddedProjects)
		{
			if (auto It = m_ProjectStore.m_Projects.find(AddedProject); It != m_ProjectStore.m_Projects.end())
			{
				ProjectStore::Project& Project = *It->second;
				for (auto& OplogPair : Project.m_Oplogs)
				{
					ProjectStore::Oplog* Oplog = OplogPair.second.get();
					AddedOplogs.push_back(Oplog);
				}
			}
		}
		for (auto& ProjectPair : m_ProjectStore.m_Projects)
		{
			ProjectStore::Project&	 Project = *ProjectPair.second;
			std::vector<std::string> AddedOplogNames(Project.GetCapturedOplogs());
			for (const std::string& OplogName : AddedOplogNames)
			{
				if (auto It = Project.m_Oplogs.find(OplogName); It != Project.m_Oplogs.end())
				{
					ProjectStore::Oplog* Oplog = It->second.get();
					AddedOplogs.push_back(Oplog);
				}
			}
		}

		for (ProjectStore::Oplog* Oplog : AddedOplogs)
		{
			size_t BaseReferenceCount = m_References.size();

			Stopwatch  InnerTimer;
			const auto __ = MakeGuard([&] {
				if (!Ctx.Settings.Verbose)
				{
					return;
				}
				ZEN_INFO("GCV2: projectstore [LOCKSTATE] '{}': found {} references in {} from {}",
						 Oplog->m_BasePath,
						 m_References.size() - BaseReferenceCount,
						 NiceTimeSpanMs(InnerTimer.GetElapsedTimeMs()),
						 Oplog->OplogId());
			});

			Oplog->IterateOplogLocked([&](const CbObjectView& UpdateOp) -> bool {
				UpdateOp.IterateAttachments([&](CbFieldView Visitor) { m_References.emplace_back(Visitor.AsAttachment()); });
				return true;
			});
		}
	}

	virtual void RemoveUsedReferencesFromSet(GcCtx& Ctx, HashSet& IoCids) override
	{
		ZEN_TRACE_CPU("Store::RemoveUsedReferencesFromSet");

		size_t	   InitialCount = IoCids.size();
		Stopwatch  Timer;
		const auto _ = MakeGuard([&] {
			if (!Ctx.Settings.Verbose)
			{
				return;
			}
			ZEN_INFO("GCV2: projectstore [FILTER REFERENCES] '{}': filtered out {} used references out of {} in {}",
					 "projectstore",
					 InitialCount - IoCids.size(),
					 InitialCount,
					 NiceTimeSpanMs(Timer.GetElapsedTimeMs()));
		});

		for (const IoHash& ReferenceHash : m_References)
		{
			if (IoCids.erase(ReferenceHash) == 1)
			{
				if (IoCids.empty())
				{
					return;
				}
			}
		}
	}

private:
	ProjectStore&		m_ProjectStore;
	std::vector<IoHash> m_References;
};

class ProjectStoreOplogReferenceChecker : public GcReferenceChecker
{
public:
	ProjectStoreOplogReferenceChecker(ProjectStore& InProjectStore, Ref<ProjectStore::Project> InProject, std::string_view InOplog)
	: m_ProjectStore(InProjectStore)
	, m_Project(InProject)
	, m_OplogName(InOplog)
	{
		m_Project->EnableUpdateCapture();
	}

	virtual ~ProjectStoreOplogReferenceChecker()
	{
		try
		{
			m_Project->DisableUpdateCapture();
			if (m_OplogCaptureEnabled)
			{
				ZEN_ASSERT(m_Oplog);
				m_Oplog->DisableUpdateCapture();
			}
		}
		catch (const std::exception& Ex)
		{
			ZEN_ERROR("~ProjectStoreOplogReferenceChecker threw exception: '{}'", Ex.what());
		}
	}

	virtual std::string GetGcName(GcCtx&) override { return fmt::format("oplog: '{}/{}'", m_Project->Identifier, m_OplogName); }

	virtual void PreCache(GcCtx& Ctx) override
	{
		ZEN_TRACE_CPU("Store::Oplog::PreCache");

		Stopwatch  Timer;
		const auto _ = MakeGuard([&] {
			if (!Ctx.Settings.Verbose)
			{
				return;
			}
			if (m_Oplog)
			{
				ZEN_INFO("GCV2: projectstore [PRECACHE] '{}': precached {} references in {} from {}/{}",
						 m_Oplog->m_BasePath,
						 m_References.size(),
						 NiceTimeSpanMs(Timer.GetElapsedTimeMs()),
						 m_Project->Identifier,
						 m_Oplog->OplogId());
			}
		});

		if (auto It = m_Project->m_Oplogs.find(m_OplogName); It != m_Project->m_Oplogs.end())
		{
			m_Oplog = It->second.get();
			m_Oplog->EnableUpdateCapture();
			m_OplogCaptureEnabled = true;

			RwLock::SharedLockScope __(m_Oplog->m_OplogLock);
			if (Ctx.IsCancelledFlag)
			{
				return;
			}
			m_Oplog->IterateOplog([&](CbObjectView Op) {
				Op.IterateAttachments([&](CbFieldView Visitor) { m_References.emplace_back(Visitor.AsAttachment()); });
			});
		}
	}

	virtual void UpdateLockedState(GcCtx& Ctx) override
	{
		ZEN_TRACE_CPU("Store::Oplog::UpdateLockedState");
		if (!m_Oplog)
		{
			return;
		}

		Stopwatch  Timer;
		const auto _ = MakeGuard([&] {
			if (!Ctx.Settings.Verbose)
			{
				return;
			}
			ZEN_INFO("GCV2: projectstore [LOCKSTATE] '{}': found {} references in {} from {}/{}",
					 m_Oplog->m_BasePath,
					 m_References.size(),
					 NiceTimeSpanMs(Timer.GetElapsedTimeMs()),
					 m_Project->Identifier,
					 m_Oplog->OplogId());
		});

		m_Oplog->IterateCapturedLSNs([&](const CbObjectView& UpdateOp) -> bool {
			UpdateOp.IterateAttachments([&](CbFieldView Visitor) { m_References.emplace_back(Visitor.AsAttachment()); });
			return true;
		});
		std::vector<IoHash> AddedAttachments = m_Oplog->GetCapturedAttachments();
		m_References.insert(m_References.end(), AddedAttachments.begin(), AddedAttachments.end());
	}

	virtual void RemoveUsedReferencesFromSet(GcCtx& Ctx, HashSet& IoCids) override
	{
		ZEN_TRACE_CPU("Store::Oplog::RemoveUsedReferencesFromSet");
		if (!m_Oplog)
		{
			return;
		}

		size_t	   InitialCount = IoCids.size();
		Stopwatch  Timer;
		const auto _ = MakeGuard([&] {
			if (!Ctx.Settings.Verbose)
			{
				return;
			}
			ZEN_INFO("GCV2: projectstore [FILTER REFERENCES] '{}': filtered out {} used references out of {} in {}",
					 m_Oplog->m_BasePath,
					 InitialCount - IoCids.size(),
					 InitialCount,
					 NiceTimeSpanMs(Timer.GetElapsedTimeMs()));
		});

		for (const IoHash& ReferenceHash : m_References)
		{
			if (IoCids.erase(ReferenceHash) == 1)
			{
				if (IoCids.empty())
				{
					return;
				}
			}
		}
	}
	ProjectStore&			   m_ProjectStore;
	Ref<ProjectStore::Project> m_Project;
	std::string				   m_OplogName;
	ProjectStore::Oplog*	   m_Oplog = nullptr;
	std::vector<IoHash>		   m_References;
	bool					   m_OplogCaptureEnabled = false;
};

std::vector<GcReferenceChecker*>
ProjectStore::CreateReferenceCheckers(GcCtx& Ctx)
{
	ZEN_TRACE_CPU("Store::CreateReferenceCheckers");

	size_t ProjectCount = 0;
	size_t OplogCount	= 0;

	Stopwatch  Timer;
	const auto _ = MakeGuard([&] {
		if (!Ctx.Settings.Verbose)
		{
			return;
		}
		ZEN_INFO("GCV2: projectstore [CREATE CHECKERS] '{}': opened {} projects and {} oplogs in {}",
				 m_ProjectBasePath,
				 ProjectCount,
				 OplogCount,
				 NiceTimeSpanMs(Timer.GetElapsedTimeMs()));
	});

	DiscoverProjects();

	std::vector<Ref<ProjectStore::Project>> Projects;
	std::vector<GcReferenceChecker*>		Checkers;
	Checkers.emplace_back(new ProjectStoreReferenceChecker(*this));
	{
		RwLock::SharedLockScope Lock(m_ProjectsLock);
		Projects.reserve(m_Projects.size());

		for (auto& Kv : m_Projects)
		{
			Projects.push_back(Kv.second);
		}
	}
	ProjectCount += Projects.size();
	try
	{
		for (const Ref<ProjectStore::Project>& Project : Projects)
		{
			std::vector<std::string> OpLogs = Project->ScanForOplogs();
			Checkers.reserve(Checkers.size() + OpLogs.size());
			for (const std::string& OpLogId : OpLogs)
			{
				Checkers.emplace_back(new ProjectStoreOplogReferenceChecker(*this, Project, OpLogId));
				OplogCount++;
			}
		}
	}
	catch (const std::exception&)
	{
		while (!Checkers.empty())
		{
			delete Checkers.back();
			Checkers.pop_back();
		}
		throw;
	}

	return Checkers;
}

std::vector<RwLock::SharedLockScope>
ProjectStore::LockState(GcCtx&)
{
	std::vector<RwLock::SharedLockScope> Locks;
	Locks.emplace_back(RwLock::SharedLockScope(m_ProjectsLock));
	for (auto& ProjectIt : m_Projects)
	{
		std::vector<RwLock::SharedLockScope> ProjectLocks = ProjectIt.second->GetGcReferencerLocks();
		for (auto It = std::make_move_iterator(ProjectLocks.begin()); It != std::make_move_iterator(ProjectLocks.end()); It++)
		{
			Locks.emplace_back(std::move(*It));
		}
	}
	return Locks;
}

//////////////////////////////////////////////////////////////////////////

#if ZEN_WITH_TESTS

namespace testutils {
	using namespace std::literals;

	std::string OidAsString(const Oid& Id)
	{
		StringBuilder<25> OidStringBuilder;
		Id.ToString(OidStringBuilder);
		return OidStringBuilder.ToString();
	}

	CbPackage CreateOplogPackage(const Oid& Id, const std::span<const std::pair<Oid, CompressedBuffer>>& Attachments)
	{
		CbPackage	   Package;
		CbObjectWriter Object;
		Object << "key"sv << OidAsString(Id);
		if (!Attachments.empty())
		{
			Object.BeginArray("bulkdata");
			for (const auto& Attachment : Attachments)
			{
				CbAttachment Attach(Attachment.second, Attachment.second.DecodeRawHash());
				Object.BeginObject();
				Object << "id"sv << Attachment.first;
				Object << "type"sv
					   << "Standard"sv;
				Object << "data"sv << Attach;
				Object.EndObject();

				Package.AddAttachment(Attach);
			}
			Object.EndArray();
		}
		Package.SetObject(Object.Save());
		return Package;
	};

	std::vector<std::pair<Oid, CompressedBuffer>> CreateAttachments(
		const std::span<const size_t>& Sizes,
		OodleCompressionLevel		   CompressionLevel = OodleCompressionLevel::VeryFast)
	{
		std::vector<std::pair<Oid, CompressedBuffer>> Result;
		Result.reserve(Sizes.size());
		for (size_t Size : Sizes)
		{
			CompressedBuffer Compressed =
				CompressedBuffer::Compress(SharedBuffer(CreateRandomBlob(Size)), OodleCompressor::Mermaid, CompressionLevel);
			Result.emplace_back(std::pair<Oid, CompressedBuffer>(Oid::NewOid(), Compressed));
		}
		return Result;
	}

	uint64_t GetCompressedOffset(const CompressedBuffer& Buffer, uint64_t RawOffset)
	{
		if (RawOffset > 0)
		{
			uint64_t			  BlockSize = 0;
			OodleCompressor		  Compressor;
			OodleCompressionLevel CompressionLevel;
			if (!Buffer.TryGetCompressParameters(Compressor, CompressionLevel, BlockSize))
			{
				return 0;
			}
			return BlockSize > 0 ? RawOffset % BlockSize : 0;
		}
		return 0;
	}

}  // namespace testutils

TEST_CASE("project.store.create")
{
	using namespace std::literals;

	ScopedTemporaryDirectory TempDir;

	auto				  JobQueue = MakeJobQueue(1, ""sv);
	GcManager			  Gc;
	CidStore			  CidStore(Gc);
	CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas", .TinyValueThreshold = 1024, .HugeValueThreshold = 4096};
	CidStore.Initialize(CidConfig);

	std::string_view	  ProjectName("proj1"sv);
	std::filesystem::path BasePath = TempDir.Path() / "projectstore";
	ProjectStore		  ProjectStore(CidStore, BasePath, Gc, *JobQueue);
	std::filesystem::path RootDir		  = TempDir.Path() / "root";
	std::filesystem::path EngineRootDir	  = TempDir.Path() / "engine";
	std::filesystem::path ProjectRootDir  = TempDir.Path() / "game";
	std::filesystem::path ProjectFilePath = TempDir.Path() / "game" / "game.uproject";

	Ref<ProjectStore::Project> Project(ProjectStore.NewProject(BasePath / ProjectName,
															   ProjectName,
															   RootDir.string(),
															   EngineRootDir.string(),
															   ProjectRootDir.string(),
															   ProjectFilePath.string()));
	CHECK(ProjectStore.DeleteProject(ProjectName));
	CHECK(!Project->Exists(BasePath));
}

TEST_CASE("project.store.lifetimes")
{
	using namespace std::literals;

	ScopedTemporaryDirectory TempDir;

	auto				  JobQueue = MakeJobQueue(1, ""sv);
	GcManager			  Gc;
	CidStore			  CidStore(Gc);
	CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas", .TinyValueThreshold = 1024, .HugeValueThreshold = 4096};
	CidStore.Initialize(CidConfig);

	std::filesystem::path BasePath = TempDir.Path() / "projectstore";
	ProjectStore		  ProjectStore(CidStore, BasePath, Gc, *JobQueue);
	std::filesystem::path RootDir		  = TempDir.Path() / "root";
	std::filesystem::path EngineRootDir	  = TempDir.Path() / "engine";
	std::filesystem::path ProjectRootDir  = TempDir.Path() / "game";
	std::filesystem::path ProjectFilePath = TempDir.Path() / "game" / "game.uproject";

	Ref<ProjectStore::Project> Project(ProjectStore.NewProject(BasePath / "proj1"sv,
															   "proj1"sv,
															   RootDir.string(),
															   EngineRootDir.string(),
															   ProjectRootDir.string(),
															   ProjectFilePath.string()));
	ProjectStore::Oplog*	   Oplog = Project->NewOplog("oplog1", {});
	CHECK(Oplog != nullptr);

	std::filesystem::path DeletePath;
	CHECK(Project->PrepareForDelete(DeletePath));
	CHECK(!DeletePath.empty());
	CHECK(Project->OpenOplog("oplog1") == nullptr);
	// Oplog is now invalid, but pointer can still be accessed since we store old oplog pointers
	CHECK(Oplog->OplogCount() == 0);
	// Project is still valid since we have a Ref to it
	CHECK(Project->Identifier == "proj1"sv);
}

struct ExportForceDisableBlocksTrue_ForceTempBlocksFalse
{
	static const bool ForceDisableBlocks	= true;
	static const bool ForceEnableTempBlocks = false;
};

struct ExportForceDisableBlocksFalse_ForceTempBlocksFalse
{
	static const bool ForceDisableBlocks	= false;
	static const bool ForceEnableTempBlocks = false;
};

struct ExportForceDisableBlocksFalse_ForceTempBlocksTrue
{
	static const bool ForceDisableBlocks	= false;
	static const bool ForceEnableTempBlocks = true;
};

TEST_CASE_TEMPLATE("project.store.export",
				   Settings,
				   ExportForceDisableBlocksTrue_ForceTempBlocksFalse,
				   ExportForceDisableBlocksFalse_ForceTempBlocksFalse,
				   ExportForceDisableBlocksFalse_ForceTempBlocksTrue)
{
	using namespace std::literals;
	using namespace testutils;

	ScopedTemporaryDirectory TempDir;
	ScopedTemporaryDirectory ExportDir;

	auto				  JobQueue = MakeJobQueue(1, ""sv);
	GcManager			  Gc;
	CidStore			  CidStore(Gc);
	CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas", .TinyValueThreshold = 1024, .HugeValueThreshold = 4096};
	CidStore.Initialize(CidConfig);

	std::filesystem::path BasePath = TempDir.Path() / "projectstore";
	ProjectStore		  ProjectStore(CidStore, BasePath, Gc, *JobQueue);
	std::filesystem::path RootDir		  = TempDir.Path() / "root";
	std::filesystem::path EngineRootDir	  = TempDir.Path() / "engine";
	std::filesystem::path ProjectRootDir  = TempDir.Path() / "game";
	std::filesystem::path ProjectFilePath = TempDir.Path() / "game" / "game.uproject";

	Ref<ProjectStore::Project> Project(ProjectStore.NewProject(BasePath / "proj1"sv,
															   "proj1"sv,
															   RootDir.string(),
															   EngineRootDir.string(),
															   ProjectRootDir.string(),
															   ProjectFilePath.string()));
	ProjectStore::Oplog*	   Oplog = Project->NewOplog("oplog1", {});
	CHECK(Oplog != nullptr);

	Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), {}));
	Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{77})));
	Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{7123, 583, 690, 99})));
	Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{55, 122})));
	Oplog->AppendNewOplogEntry(
		CreateOplogPackage(Oid::NewOid(),
						   CreateAttachments(std::initializer_list<size_t>{256u * 1024u, 92u * 1024u}, OodleCompressionLevel::None)));

	FileRemoteStoreOptions Options = {
		RemoteStoreOptions{.MaxBlockSize = 64u * 1024, .MaxChunkEmbedSize = 32 * 1024u, .ChunkFileSizeLimit = 64u * 1024u},
		/*.FolderPath = */ ExportDir.Path(),
		/*.Name = */ std::string("oplog1"),
		/*OptionalBaseName = */ std::string(),
		/*.ForceDisableBlocks = */ Settings::ForceDisableBlocks,
		/*.ForceEnableTempBlocks = */ Settings::ForceEnableTempBlocks};
	std::shared_ptr<RemoteProjectStore> RemoteStore = CreateFileRemoteStore(Options);
	RemoteProjectStore::RemoteStoreInfo StoreInfo	= RemoteStore->GetInfo();

	RemoteProjectStore::Result ExportResult = SaveOplog(CidStore,
														*RemoteStore,
														*Project.Get(),
														*Oplog,
														Options.MaxBlockSize,
														Options.MaxChunkEmbedSize,
														Options.ChunkFileSizeLimit,
														true,
														false,
														false,
														nullptr);

	CHECK(ExportResult.ErrorCode == 0);

	ProjectStore::Oplog* OplogImport = Project->NewOplog("oplog2", {});
	CHECK(OplogImport != nullptr);

	RemoteProjectStore::Result ImportResult =
		LoadOplog(CidStore, *RemoteStore, *OplogImport, /*Force*/ false, /*IgnoreMissingAttachments*/ false, /*CleanOplog*/ false, nullptr);
	CHECK(ImportResult.ErrorCode == 0);

	RemoteProjectStore::Result ImportForceResult =
		LoadOplog(CidStore, *RemoteStore, *OplogImport, /*Force*/ true, /*IgnoreMissingAttachments*/ false, /*CleanOplog*/ false, nullptr);
	CHECK(ImportForceResult.ErrorCode == 0);

	RemoteProjectStore::Result ImportCleanResult =
		LoadOplog(CidStore, *RemoteStore, *OplogImport, /*Force*/ false, /*IgnoreMissingAttachments*/ false, /*CleanOplog*/ true, nullptr);
	CHECK(ImportCleanResult.ErrorCode == 0);

	RemoteProjectStore::Result ImportForceCleanResult =
		LoadOplog(CidStore, *RemoteStore, *OplogImport, /*Force*/ true, /*IgnoreMissingAttachments*/ false, /*CleanOplog*/ true, nullptr);
	CHECK(ImportForceCleanResult.ErrorCode == 0);
}

TEST_CASE("project.store.gc")
{
	using namespace std::literals;
	using namespace testutils;

	ScopedTemporaryDirectory TempDir;

	auto				  JobQueue = MakeJobQueue(1, ""sv);
	GcManager			  Gc;
	CidStore			  CidStore(Gc);
	CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas", .TinyValueThreshold = 1024, .HugeValueThreshold = 4096};
	CidStore.Initialize(CidConfig);

	std::filesystem::path BasePath = TempDir.Path() / "projectstore";
	ProjectStore		  ProjectStore(CidStore, BasePath, Gc, *JobQueue);
	std::filesystem::path RootDir		= TempDir.Path() / "root";
	std::filesystem::path EngineRootDir = TempDir.Path() / "engine";

	std::filesystem::path Project1RootDir  = TempDir.Path() / "game1";
	std::filesystem::path Project1FilePath = TempDir.Path() / "game1" / "game.uproject";
	{
		CreateDirectories(Project1FilePath.parent_path());
		BasicFile ProjectFile;
		ProjectFile.Open(Project1FilePath, BasicFile::Mode::kTruncate);
	}
	std::filesystem::path Project1OplogPath = TempDir.Path() / "game1" / "saves" / "cooked" / ".projectstore";
	{
		CreateDirectories(Project1OplogPath.parent_path());
		BasicFile OplogFile;
		OplogFile.Open(Project1OplogPath, BasicFile::Mode::kTruncate);
	}

	std::filesystem::path Project2RootDir  = TempDir.Path() / "game2";
	std::filesystem::path Project2FilePath = TempDir.Path() / "game2" / "game.uproject";
	{
		CreateDirectories(Project2FilePath.parent_path());
		BasicFile ProjectFile;
		ProjectFile.Open(Project2FilePath, BasicFile::Mode::kTruncate);
	}
	std::filesystem::path Project2Oplog1Path = TempDir.Path() / "game1" / "saves" / "cooked" / ".projectstore";
	{
		CreateDirectories(Project2Oplog1Path.parent_path());
		BasicFile OplogFile;
		OplogFile.Open(Project2Oplog1Path, BasicFile::Mode::kTruncate);
	}
	std::filesystem::path Project2Oplog2Path = TempDir.Path() / "game2" / "saves" / "cooked" / ".projectstore";
	{
		CreateDirectories(Project2Oplog2Path.parent_path());
		BasicFile OplogFile;
		OplogFile.Open(Project2Oplog2Path, BasicFile::Mode::kTruncate);
	}

	{
		Ref<ProjectStore::Project> Project1(ProjectStore.NewProject(BasePath / "proj1"sv,
																	"proj1"sv,
																	RootDir.string(),
																	EngineRootDir.string(),
																	Project1RootDir.string(),
																	Project1FilePath.string()));
		ProjectStore::Oplog*	   Oplog = Project1->NewOplog("oplog1", Project1OplogPath);
		CHECK(Oplog != nullptr);

		Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), {}));
		Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{77})));
		Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{7123, 583, 690, 99})));
		Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{55, 122})));
	}

	{
		Ref<ProjectStore::Project> Project2(ProjectStore.NewProject(BasePath / "proj2"sv,
																	"proj2"sv,
																	RootDir.string(),
																	EngineRootDir.string(),
																	Project2RootDir.string(),
																	Project2FilePath.string()));
		{
			ProjectStore::Oplog* Oplog = Project2->NewOplog("oplog2", Project2Oplog1Path);
			CHECK(Oplog != nullptr);

			Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), {}));
			Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{177})));
			Oplog->AppendNewOplogEntry(
				CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{9123, 383, 590, 96})));
			Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{535, 221})));
		}
		{
			ProjectStore::Oplog* Oplog = Project2->NewOplog("oplog3", Project2Oplog2Path);
			CHECK(Oplog != nullptr);

			Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), {}));
			Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{137})));
			Oplog->AppendNewOplogEntry(
				CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{9723, 683, 594, 98})));
			Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{531, 271})));
		}
	}

	SUBCASE("v1")
	{
		{
			GcContext GcCtx(GcClock::Now() - std::chrono::hours(24), GcClock::Now() - std::chrono::hours(24));
			ProjectStore.GatherReferences(GcCtx);
			size_t RefCount = 0;
			GcCtx.IterateCids([&RefCount](const IoHash&) { RefCount++; });
			CHECK(RefCount == 21);
			ProjectStore.CollectGarbage(GcCtx);
			CHECK(ProjectStore.OpenProject("proj1"sv));
			CHECK(ProjectStore.OpenProject("proj2"sv));
		}

		{
			GcContext GcCtx(GcClock::Now() + std::chrono::hours(24), GcClock::Now() + std::chrono::hours(24));
			ProjectStore.GatherReferences(GcCtx);
			size_t RefCount = 0;
			GcCtx.IterateCids([&RefCount](const IoHash&) { RefCount++; });
			CHECK(RefCount == 21);
			ProjectStore.CollectGarbage(GcCtx);
			CHECK(ProjectStore.OpenProject("proj1"sv));
			CHECK(ProjectStore.OpenProject("proj2"sv));
		}

		std::filesystem::remove(Project1FilePath);

		{
			GcContext GcCtx(GcClock::Now() - std::chrono::hours(24), GcClock::Now() - std::chrono::hours(24));
			ProjectStore.GatherReferences(GcCtx);
			size_t RefCount = 0;
			GcCtx.IterateCids([&RefCount](const IoHash&) { RefCount++; });
			CHECK(RefCount == 21);
			ProjectStore.CollectGarbage(GcCtx);
			CHECK(ProjectStore.OpenProject("proj1"sv));
			CHECK(ProjectStore.OpenProject("proj2"sv));
		}

		{
			GcContext GcCtx(GcClock::Now() + std::chrono::hours(24), GcClock::Now() + std::chrono::hours(24));
			ProjectStore.GatherReferences(GcCtx);
			size_t RefCount = 0;
			GcCtx.IterateCids([&RefCount](const IoHash&) { RefCount++; });
			CHECK(RefCount == 14);
			ProjectStore.CollectGarbage(GcCtx);
			CHECK(!ProjectStore.OpenProject("proj1"sv));
			CHECK(ProjectStore.OpenProject("proj2"sv));
		}

		std::filesystem::remove(Project2Oplog1Path);
		{
			GcContext GcCtx(GcClock::Now() - std::chrono::hours(24), GcClock::Now() - std::chrono::hours(24));
			ProjectStore.GatherReferences(GcCtx);
			size_t RefCount = 0;
			GcCtx.IterateCids([&RefCount](const IoHash&) { RefCount++; });
			CHECK(RefCount == 14);
			ProjectStore.CollectGarbage(GcCtx);
			CHECK(!ProjectStore.OpenProject("proj1"sv));
			CHECK(ProjectStore.OpenProject("proj2"sv));
		}

		{
			GcContext GcCtx(GcClock::Now() + std::chrono::hours(24), GcClock::Now() + std::chrono::hours(24));
			ProjectStore.GatherReferences(GcCtx);
			size_t RefCount = 0;
			GcCtx.IterateCids([&RefCount](const IoHash&) { RefCount++; });
			CHECK(RefCount == 7);
			ProjectStore.CollectGarbage(GcCtx);
			CHECK(!ProjectStore.OpenProject("proj1"sv));
			CHECK(ProjectStore.OpenProject("proj2"sv));
		}

		std::filesystem::remove(Project2FilePath);
		{
			GcContext GcCtx(GcClock::Now() + std::chrono::hours(24), GcClock::Now() + std::chrono::hours(24));
			ProjectStore.GatherReferences(GcCtx);
			size_t RefCount = 0;
			GcCtx.IterateCids([&RefCount](const IoHash&) { RefCount++; });
			CHECK(RefCount == 0);
			ProjectStore.CollectGarbage(GcCtx);
			CHECK(!ProjectStore.OpenProject("proj1"sv));
			CHECK(!ProjectStore.OpenProject("proj2"sv));
		}
	}

	SUBCASE("v2")
	{
		{
			GcSettings Settings = {.CacheExpireTime		   = GcClock::Now() - std::chrono::hours(24),
								   .ProjectStoreExpireTime = GcClock::Now() - std::chrono::hours(24),
								   .IsDeleteMode		   = true};
			GcResult   Result	= Gc.CollectGarbage(Settings);
			CHECK_EQ(5u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount);
			CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount);
			CHECK_EQ(21u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount);
			CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount);
			CHECK(ProjectStore.OpenProject("proj1"sv));
			CHECK(ProjectStore.OpenProject("proj2"sv));
		}

		{
			GcSettings Settings = {.CacheExpireTime		   = GcClock::Now() + std::chrono::hours(24),
								   .ProjectStoreExpireTime = GcClock::Now() + std::chrono::hours(24),
								   .IsDeleteMode		   = true};
			GcResult   Result	= Gc.CollectGarbage(Settings);
			CHECK_EQ(5u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount);
			CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount);
			CHECK_EQ(21u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount);
			CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount);
			CHECK(ProjectStore.OpenProject("proj1"sv));
			CHECK(ProjectStore.OpenProject("proj2"sv));
		}

		std::filesystem::remove(Project1FilePath);

		{
			GcSettings Settings = {.CacheExpireTime		   = GcClock::Now() - std::chrono::hours(24),
								   .ProjectStoreExpireTime = GcClock::Now() - std::chrono::hours(24),
								   .IsDeleteMode		   = true};
			GcResult   Result	= Gc.CollectGarbage(Settings);
			CHECK_EQ(5u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount);
			CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount);
			CHECK_EQ(21u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount);
			CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount);
			CHECK(ProjectStore.OpenProject("proj1"sv));
			CHECK(ProjectStore.OpenProject("proj2"sv));
		}

		{
			GcSettings Settings = {.CacheExpireTime		   = GcClock::Now() + std::chrono::hours(24),
								   .ProjectStoreExpireTime = GcClock::Now() + std::chrono::hours(24),
								   .CollectSmallObjects	   = true,
								   .IsDeleteMode		   = true};
			GcResult   Result	= Gc.CollectGarbage(Settings);
			CHECK_EQ(4u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount);
			CHECK_EQ(1u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount);
			CHECK_EQ(21u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount);
			CHECK_EQ(7u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount);
			CHECK(!ProjectStore.OpenProject("proj1"sv));
			CHECK(ProjectStore.OpenProject("proj2"sv));
		}

		std::filesystem::remove(Project2Oplog1Path);
		{
			GcSettings Settings = {.CacheExpireTime		   = GcClock::Now() - std::chrono::hours(24),
								   .ProjectStoreExpireTime = GcClock::Now() - std::chrono::hours(24),
								   .CollectSmallObjects	   = true,
								   .IsDeleteMode		   = true};
			GcResult   Result	= Gc.CollectGarbage(Settings);
			CHECK_EQ(3u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount);
			CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount);
			CHECK_EQ(14u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount);
			CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount);
			CHECK(!ProjectStore.OpenProject("proj1"sv));
			CHECK(ProjectStore.OpenProject("proj2"sv));
		}

		{
			GcSettings Settings = {.CacheExpireTime		   = GcClock::Now() + std::chrono::hours(24),
								   .ProjectStoreExpireTime = GcClock::Now() + std::chrono::hours(24),
								   .CollectSmallObjects	   = true,
								   .IsDeleteMode		   = true};
			GcResult   Result	= Gc.CollectGarbage(Settings);
			CHECK_EQ(3u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount);
			CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount);
			CHECK_EQ(14u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount);
			CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount);
			CHECK(!ProjectStore.OpenProject("proj1"sv));
			CHECK(ProjectStore.OpenProject("proj2"sv));
		}

		std::filesystem::remove(Project2FilePath);
		{
			GcSettings Settings = {.CacheExpireTime		   = GcClock::Now() + std::chrono::hours(24),
								   .ProjectStoreExpireTime = GcClock::Now() + std::chrono::hours(24),
								   .CollectSmallObjects	   = true,
								   .IsDeleteMode		   = true};
			GcResult   Result	= Gc.CollectGarbage(Settings);
			CHECK_EQ(1u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount);
			CHECK_EQ(1u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount);
			CHECK_EQ(14u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount);
			CHECK_EQ(14u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount);
			CHECK(!ProjectStore.OpenProject("proj1"sv));
			CHECK(!ProjectStore.OpenProject("proj2"sv));
		}
	}
}

TEST_CASE("project.store.partial.read")
{
	using namespace std::literals;
	using namespace testutils;

	ScopedTemporaryDirectory TempDir;

	auto				  JobQueue = MakeJobQueue(1, ""sv);
	GcManager			  Gc;
	CidStore			  CidStore(Gc);
	CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas"sv, .TinyValueThreshold = 1024, .HugeValueThreshold = 4096};
	CidStore.Initialize(CidConfig);

	std::filesystem::path BasePath = TempDir.Path() / "projectstore"sv;
	ProjectStore		  ProjectStore(CidStore, BasePath, Gc, *JobQueue);
	std::filesystem::path RootDir		= TempDir.Path() / "root"sv;
	std::filesystem::path EngineRootDir = TempDir.Path() / "engine"sv;

	std::filesystem::path Project1RootDir  = TempDir.Path() / "game1"sv;
	std::filesystem::path Project1FilePath = TempDir.Path() / "game1"sv / "game.uproject"sv;
	{
		CreateDirectories(Project1FilePath.parent_path());
		BasicFile ProjectFile;
		ProjectFile.Open(Project1FilePath, BasicFile::Mode::kTruncate);
	}

	std::vector<Oid> OpIds;
	OpIds.insert(OpIds.end(), {Oid::NewOid(), Oid::NewOid(), Oid::NewOid(), Oid::NewOid()});
	std::unordered_map<Oid, std::vector<std::pair<Oid, CompressedBuffer>>, Oid::Hasher> Attachments;
	{
		Ref<ProjectStore::Project> Project1(ProjectStore.NewProject(BasePath / "proj1"sv,
																	"proj1"sv,
																	RootDir.string(),
																	EngineRootDir.string(),
																	Project1RootDir.string(),
																	Project1FilePath.string()));
		ProjectStore::Oplog*	   Oplog = Project1->NewOplog("oplog1"sv, {});
		CHECK(Oplog != nullptr);
		Attachments[OpIds[0]] = {};
		Attachments[OpIds[1]] = CreateAttachments(std::initializer_list<size_t>{77});
		Attachments[OpIds[2]] = CreateAttachments(std::initializer_list<size_t>{7123, 9583, 690, 99});
		Attachments[OpIds[3]] = CreateAttachments(std::initializer_list<size_t>{55, 122});
		for (auto It : Attachments)
		{
			Oplog->AppendNewOplogEntry(CreateOplogPackage(It.first, It.second));
		}
	}
	{
		IoBuffer Chunk;
		CHECK(ProjectStore
				  .GetChunk("proj1"sv,
							"oplog1"sv,
							Attachments[OpIds[1]][0].second.DecodeRawHash().ToHexString(),
							HttpContentType::kCompressedBinary,
							Chunk)
				  .first == HttpResponseCode::OK);
		IoHash			 RawHash;
		uint64_t		 RawSize;
		CompressedBuffer Attachment = CompressedBuffer::FromCompressed(SharedBuffer(Chunk), RawHash, RawSize);
		CHECK(RawSize == Attachments[OpIds[1]][0].second.DecodeRawSize());
	}

	CompositeBuffer ChunkResult;
	HttpContentType ContentType;
	CHECK(ProjectStore
			  .GetChunkRange("proj1"sv,
							 "oplog1"sv,
							 OidAsString(Attachments[OpIds[2]][1].first),
							 0,
							 ~0ull,
							 HttpContentType::kCompressedBinary,
							 ChunkResult,
							 ContentType)
			  .first == HttpResponseCode::OK);
	CHECK(ChunkResult);
	CHECK(CompressedBuffer::FromCompressedNoValidate(std::move(ChunkResult)).DecodeRawSize() ==
		  Attachments[OpIds[2]][1].second.DecodeRawSize());

	CompositeBuffer PartialChunkResult;
	CHECK(ProjectStore
			  .GetChunkRange("proj1"sv,
							 "oplog1"sv,
							 OidAsString(Attachments[OpIds[2]][1].first),
							 5,
							 1773,
							 HttpContentType::kCompressedBinary,
							 PartialChunkResult,
							 ContentType)
			  .first == HttpResponseCode::OK);
	CHECK(PartialChunkResult);
	IoHash			 PartialRawHash;
	uint64_t		 PartialRawSize;
	CompressedBuffer PartialCompressedResult = CompressedBuffer::FromCompressed(PartialChunkResult, PartialRawHash, PartialRawSize);
	CHECK(PartialRawSize >= 1773);

	uint64_t	   RawOffsetInPartialCompressed = GetCompressedOffset(PartialCompressedResult, 5);
	SharedBuffer   PartialDecompressed			= PartialCompressedResult.Decompress(RawOffsetInPartialCompressed);
	SharedBuffer   FullDecompressed				= Attachments[OpIds[2]][1].second.Decompress();
	const uint8_t* FullDataPtr					= &(reinterpret_cast<const uint8_t*>(FullDecompressed.GetView().GetData())[5]);
	const uint8_t* PartialDataPtr				= reinterpret_cast<const uint8_t*>(PartialDecompressed.GetView().GetData());
	CHECK(FullDataPtr[0] == PartialDataPtr[0]);
}

TEST_CASE("project.store.block")
{
	using namespace std::literals;
	using namespace testutils;

	std::vector<std::size_t> AttachmentSizes({7633, 6825, 5738, 8031, 7225, 566,  3656, 6006, 24,	3466, 1093, 4269, 2257, 3685, 3489,
											  7194, 6151, 5482, 6217, 3511, 6738, 5061, 7537, 2759, 1916, 8210, 2235, 4024, 1582, 5251,
											  491,	5464, 4607, 8135, 3767, 4045, 4415, 5007, 8876, 6761, 3359, 8526, 4097, 4855, 8225});

	std::vector<std::pair<Oid, CompressedBuffer>>  AttachmentsWithId = CreateAttachments(AttachmentSizes);
	std::vector<std::pair<IoHash, FetchChunkFunc>> Chunks;
	Chunks.reserve(AttachmentSizes.size());
	for (const auto& It : AttachmentsWithId)
	{
		Chunks.push_back(std::make_pair(It.second.DecodeRawHash(),
										[Buffer = It.second.GetCompressed().Flatten().AsIoBuffer()](const IoHash&) -> CompositeBuffer {
											return CompositeBuffer(SharedBuffer(Buffer));
										}));
	}
	CompressedBuffer Block = GenerateBlock(std::move(Chunks));
	CHECK(IterateBlock(Block.Decompress(), [](CompressedBuffer&&, const IoHash&) {}));
}

#endif

void
prj_forcelink()
{
}

}  // namespace zen
